@maka/maka-cli 5.226.0 → 5.228.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.226.0",
3
+ "version": "5.228.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",
@@ -134,7 +134,20 @@ export class DisableCommand extends Command {
134
134
  return t.viaWan && t.host?.host ? wanDefensePool(t.device, t.host.host) : deviceDefensePool(t.device);
135
135
  }
136
136
  if (t.kind === 'camera') {
137
- return t.master?.host ? hostDefensePool(t.master.host) : { label: 'Device Rating + Firewall', pool: 4 };
137
+ // THE LENSES RATE WHAT THE SITE SAYS THEY RATE (Room.cameraRating,
138
+ // eJANPpm7qCaCAJBJw). This used to borrow the host's whole pool
139
+ // while silent-icons.ts hid the same cluster behind a hard-coded 4
140
+ // and the icon line printed no rating at all -- three answers to
141
+ // one question, two of them invisible. A slaved camera still
142
+ // borrows its master where the master is HIGHER (p.233, the same
143
+ // rule wanDefensePool applies to a slaved device), so a corp host
144
+ // still hardens its own cameras; what changed is that the floor is
145
+ // the seeded rating rather than a constant nobody could read.
146
+ const own = { label: 'Device Rating + Firewall', pool: Math.max(0, t.room.cameraRating * 2) };
147
+ if (!t.master?.host)
148
+ return own;
149
+ const viaHost = hostDefensePool(t.master.host);
150
+ return viaHost.pool >= own.pool ? viaHost : own;
138
151
  }
139
152
  return { label: 'Intuition + Firewall', pool: t.owner.getPanDefensePool('sleaze') };
140
153
  }
@@ -333,7 +333,17 @@ export class LookCommand extends Command {
333
333
  // in the icon block below rather than one look later.
334
334
  const swept = spotHiddenIcons(this.scene, this.actor, this.actor.insideHost === room ? room : gridVicinity(this.actor));
335
335
  const lines = [gridSculpt(this.actor, room)];
336
- lines.push(gridIconBlock(this.scene, this.actor, room, this.rooms));
336
+ // THE SWEEP IS REPORTED BEFORE THE LIST IT CHANGED
337
+ // (eJANPpm7qCaCAJBJw, third rejection: "I still see the Service
338
+ // Keypad and Camera cluster").
339
+ //
340
+ // The sweep runs first so anything it wins appears in this look
341
+ // rather than one look later -- that part was right. But its
342
+ // SENTENCE was pushed after the icon block, so the player read a
343
+ // newly-revealed keypad with nothing above it saying why, and the
344
+ // "you pick an icon out of the noise" line arrived two blocks down.
345
+ // Revealing something and explaining it in that order reads exactly
346
+ // like never having hidden it, which is what was reported.
337
347
  if (swept) {
338
348
  lines.push(spotLine(swept));
339
349
  if (showsMechanics(this.scene, this.actor)) {
@@ -342,6 +352,7 @@ export class LookCommand extends Command {
342
352
  }
343
353
  }
344
354
  }
355
+ lines.push(gridIconBlock(this.scene, this.actor, room, this.rooms, swept?.found));
345
356
  // THE COACHING HAS TO MATCH THE ROOM YOU ARE IN. Inside a host,
346
357
  // `go` is refused (go.ts checkHostBoundary) and `map` draws the
347
358
  // outside grid you cannot reach -- so offering both is the same
@@ -1251,9 +1251,13 @@
1251
1251
  // 'panel' -- the maglock, the keypad, the safe). A ward is the Art
1252
1252
  // and has no wireless mode to hide in.
1253
1253
  // - CAMERAS TOO, which p.217 names FIRST. The cluster is not a Device
1254
- // (it is derived from the room), so it hides on Room.camerasSpottedBy
1255
- // and resists with its master host's rating -- a site's lenses are as
1256
- // quiet as the security that runs them.
1254
+ // (it is derived from the room), so it hides on Room.camerasSpottedBy.
1255
+ // (This entry ORIGINALLY said it resisted with its MASTER HOST'S
1256
+ // rating. That was wrong and 1.80.1 removed it the same day -- a
1257
+ // lens behind a rating-8 host was a 16-die hide against a sweep
1258
+ // capped near 5, and it measured ZERO finds in twelve looks. It
1259
+ // resists with its own rating; see 1.82.0 for where that number
1260
+ // now lives.)
1257
1261
  // - AND PEOPLE. Going dark used to make an icon simply unfindable:
1258
1262
  // isLinked answers false and nothing anywhere rolled to beat it, so
1259
1263
  // the book's own example of a hidden icon was the one case with no
@@ -1325,5 +1329,47 @@
1325
1329
  // hole had been open: they leaned on the old "no gear, stay quiet"
1326
1330
  // branch and have been rolling unlimited all along. They carry decks
1327
1331
  // now. The VR gate (a direct neural interface, p.222-223) is untouched.
1328
- export const ENGINE_VERSION = '1.81.0';
1332
+ // 1.82.0 (2026-09-16): WHAT MATRIX PERCEPTION BUYS MUST BE EARNED
1333
+ // (eJANPpm7qCaCAJBJw, third rejection). 1.80.0 hid the ICON and stopped
1334
+ // there. The reporter read the transcript and found the rest:
1335
+ // "I shouldn't even see 'Service Keypad hides' at all if I don't
1336
+ // clear the firewall."
1337
+ // They were right, and the leak was in the DICE, not the prose. The
1338
+ // narrative line was correctly vague -- "something here is running
1339
+ // silent and you cannot pin it" -- and then the mechanics pane named
1340
+ // the thing on the very next line, win or lose:
1341
+ // Service Keypad hides (Device Rating (no Logic to hide behind)): 6d6 -> 2 hits
1342
+ // A FAILED SWEEP TAUGHT YOU MORE THAN A SUCCESSFUL ONE WOULD HAVE.
1343
+ // - A LOST SWEEP NAMES NOTHING. ISpotOutcome.missed is now a COUNT,
1344
+ // not a list of names, and the losing branch of the loop never puts
1345
+ // a name in the outcome at all. This is the shape of the fix that
1346
+ // matters: the old type carried the names and merely trusted every
1347
+ // caller to remember not to print them. A name that was never earned
1348
+ // cannot now be printed by accident, because it is not there. The
1349
+ // dice still show: "something holds its cover: 6d6 -> 2 hits".
1350
+ // - RATINGS ARE PROTECTED TOO, which is the reporter's other question
1351
+ // answered. p.235 sells "the target's device rating" for a hit, and
1352
+ // the engine printed [DR n] on every icon for free. It is now
1353
+ // [DR ?] until the icon is yours: ratingKnownBy = not silent, or
1354
+ // spotted by you. NOTE THE SMALLER CLAIM -- a never-silent device
1355
+ // shows its rating for nothing. p.235 charges a hit for it even
1356
+ // then; this engine does not, because a loud icon has nothing to
1357
+ // roll against and inventing a resistance for it would be inventing
1358
+ // a rule. Said plainly rather than papered over.
1359
+ // - THE CAMERA CLUSTER HAS A RATING AT LAST, which was the third
1360
+ // complaint. Room.cameraRating, ONE number, feeding all three places
1361
+ // that disagreed: what it hides behind (Device Rating + Firewall),
1362
+ // what it defends Control Device / Data Spike with, and what the
1363
+ // grid prints. It is 1 (p.356 lists Camera at 1; the same list gives
1364
+ // the door lock the 2 the engine already uses). It is a FIELD, so a
1365
+ // corp floor that wants p.421's mil-spec band is a one-line change
1366
+ // in the seed layer, not a rewrite.
1367
+ // - AND THE SWEEP READS AS A SWEEP: the narration moved ABOVE the icon
1368
+ // block (you were reading revealed icons before the sentence that
1369
+ // explained them), and an icon found this look is tagged "<- just
1370
+ // spotted, was running silent". Without the tag, every later look
1371
+ // shows it with no explanation at all -- indistinguishable from an
1372
+ // icon that was never hidden, which is most of why 1.80.0 read to
1373
+ // the reporter as nothing having happened.
1374
+ export const ENGINE_VERSION = '1.82.0';
1329
1375
  //# sourceMappingURL=engine-version.js.map
@@ -12,6 +12,13 @@ import { AbstractRoom } from '../types/shared/abstracts.js';
12
12
  import { spotsActive, actorDistanceMeters, voiceBandFor, voiceTag, doorwayKindOf, exitDoorwayKind } from '../utilities/spots.js';
13
13
  import { canHearSpeech, canHearShout } from '../utilities/earshot.js';
14
14
  import { synthesizeGrid, attachExitCell, detachExitCell, } from '../utilities/room-grid.js';
15
+ /**
16
+ * A CAMERA'S DEVICE RATING when the scene does not say (p.356, which
17
+ * lists "Camera" at 1 -- the same table that gives the door lock the 2
18
+ * DEFAULT_DEVICE_RATING already uses). A seed raises it: p.421 bands
19
+ * ordinary security devices at 2 and mil-spec at 4.
20
+ */
21
+ export const DEFAULT_CAMERA_RATING = 1;
15
22
  export class Room extends AbstractRoom {
16
23
  logger = Logger.getInstance();
17
24
  actors = new Set();
@@ -74,6 +81,29 @@ export class Room extends AbstractRoom {
74
81
  * of the grid.
75
82
  */
76
83
  camerasSpottedBy = new Set();
84
+ /**
85
+ * WHAT THE LENSES RATE (eJANPpm7qCaCAJBJw, third rejection: "the
86
+ * camera cluster doesn't have a DR"), and the reporter's ruling that
87
+ * it should be per-site rather than one global number.
88
+ *
89
+ * The cluster is not a Device -- it is derived from the room itself
90
+ * (utilities/surveillance.ts isWatched) -- so it had no rating to
91
+ * carry, and the codebase had quietly grown THREE answers that
92
+ * disagreed: it hid behind a hard-coded 4, it defended a Data Spike
93
+ * with the HOST's pool (commands/disable.ts), and it displayed no
94
+ * rating at all. One number now feeds all three, so what a player
95
+ * reads is what it rolls.
96
+ *
97
+ * ONE FOR NOW (user ruling 2026-09-16), which is what p.356 lists a
98
+ * Camera at -- the same table the engine already takes its door-lock 2
99
+ * from. Per-site seeding was scoped out deliberately: the defect here
100
+ * was three numbers disagreeing, and one field fixes that without a
101
+ * seed knob the generator could get wrong. It is a FIELD rather than a
102
+ * constant so that making it per-site later is a one-line change at
103
+ * the seed layer and nothing else moves. p.421 bands ordinary security
104
+ * devices at 2 and mil-spec at 4 when that day comes.
105
+ */
106
+ cameraRating = DEFAULT_CAMERA_RATING;
77
107
  /**
78
108
  * THE FALLEN (player ruling 2026-08-25). A killed actor leaves a BODY
79
109
  * where they dropped, holding everything they carried, instead of
@@ -1,3 +1,39 @@
1
+ /**
2
+ * THE BENCH'S ACTOR. One name, used for the seat, for the transcript's
3
+ * visibility filter, and for the post-boot fixture state below.
4
+ *
5
+ * IT LIVES HERE, in the types module, on purpose. utilities/repro-launch
6
+ * declared it, and the launcher's whole design is that the game graph
7
+ * (repro-scene, the harness, blessed, 150 files) stays behind a dynamic
8
+ * import so `maka --help` does not pay for it. Once the site needed the
9
+ * same constant, anything that re-exported it from a module importing
10
+ * the builder would have dragged that graph back across the lazy
11
+ * boundary. This file imports nothing, so both sides can share the name
12
+ * for free.
13
+ */
14
+ export const REPRO_ACTOR = 'Tester';
15
+ /**
16
+ * THE FIXTURE A BENCH ACTUALLY BUILDS FROM.
17
+ *
18
+ * One rule, and it is not a convenience: a bench written with
19
+ * `repro.premise` and no `fixture.premise` is the common shape, so the
20
+ * reader's framing doubles as the scene's story when the fixture has
21
+ * none of its own. Three callers build a bench -- the headless dry run,
22
+ * the CLI's live launcher, and maka-cli.com's web bench -- and each had
23
+ * to know this. Two of them spelling it the same way and a third
24
+ * forgetting is how a bench acquires a different opening line depending
25
+ * on where it was run from.
26
+ *
27
+ * Here rather than beside the builder because this file imports nothing,
28
+ * so the launcher can apply the rule without reaching across its lazy
29
+ * boundary into the game graph.
30
+ */
31
+ export function reproFixtureOf(repro) {
32
+ const fixture = { ...(repro.fixture ?? {}) };
33
+ if (!fixture.premise && repro.premise)
34
+ fixture.premise = repro.premise;
35
+ return fixture;
36
+ }
1
37
  /**
2
38
  * THE FIXTURE SCHEMA'S VERSION, and the engine side is the source of
3
39
  * truth for it. Anything that GENERATES repro drafts -- the
@@ -2,6 +2,7 @@ import { isLinked, CALL_COLOR } from './comm-style.js';
2
2
  import { isWatched, camerasLive, canSnoopFeeds } from './surveillance.js';
3
3
  import { Category } from '../types/shared/item-enum.js';
4
4
  import { hint } from './hints.js';
5
+ import { cameraDrTag, drTagFor } from './grid-view.js';
5
6
  /**
6
7
  * AROs -- AUGMENTED REALITY OBJECTS (player request 2026-08-26: "we
7
8
  * need AROs on anything online"). The 2075 street is not bare: anything
@@ -147,9 +148,10 @@ export function aroTags(scene, actor, room) {
147
148
  // The verb rides the tag: standing in the room IS the requirement
148
149
  // for a direct connection (commands/tap.ts), so this is the one
149
150
  // place the player is always in a position to use it.
151
+ const camDr = cameraDrTag(room, actor.name);
150
152
  tags.push(canSnoopFeeds(rooms, room, actor) && !camerasLive(rooms, room)
151
- ? `◎ Camera cluster -- wireless, and looping empty hallways for the house.`
152
- : `◎ Camera cluster -- wireless, and watching.${hint(` ("tap" cables into it -- a slaved lens can't hide behind its host)`)}`);
153
+ ? `◎ Camera cluster ${camDr}-- wireless, and looping empty hallways for the house.`
154
+ : `◎ Camera cluster ${camDr}-- wireless, and watching.${hint(` ("tap" cables into it -- a slaved lens can't hide behind its host)`)}`);
153
155
  }
154
156
  // THE FIXED DEVICES -- the keypad on the wall, the maglock on the
155
157
  // grate (YjBA9xhY2NDmCFw5d, Deditri: "The keypad should be
@@ -174,7 +176,7 @@ export function aroTags(scene, actor, room) {
174
176
  // still picked, breached, shot and keyed exactly as before.
175
177
  if (!device.hasIconFor(actor.name))
176
178
  continue;
177
- tags.push(`▤ ${device.name} -- wireless, {bold}[DR ${device.rating}]{/bold}${device.isOpen() ? ', standing open' : `.${hint(` ("${device.verbs()[0]} ${device.name}")`)}`}`);
179
+ tags.push(`▤ ${device.name} -- wireless, ${drTagFor(device, actor.name)}${device.isOpen() ? ', standing open' : `.${hint(` ("${device.verbs()[0]} ${device.name}")`)}`}`);
178
180
  }
179
181
  // LOOSE DEVICES lying in plain sight. A wireless deck on a table
180
182
  // announces itself; one in a drawer you never opened does not.
@@ -23,6 +23,65 @@ const NEAR_DIVIDER = '-- and NEAR it, out on the open grid (not inside the host)
23
23
  export function isNearDivider(line) {
24
24
  return line === NEAR_DIVIDER;
25
25
  }
26
+ /**
27
+ * A DEVICE RATING IS SOMETHING YOU BUY WITH A HIT, not something the
28
+ * screen gives away (eJANPpm7qCaCAJBJw -- "are device ratings
29
+ * themselves, protected information?"; p.235).
30
+ *
31
+ * p.235 lists what one hit on a Matrix Perception test buys, and "the
32
+ * target's device rating" is an entry on it, beside "the type of icon"
33
+ * and "the rating of one of the target's Matrix attributes". The engine
34
+ * printed [DR n] on every icon for nothing, which is the same defect the
35
+ * silent-icon work fixed one field over: the rating was free.
36
+ *
37
+ * WHAT COUNTS AS HAVING READ IT is the sweep that found the icon
38
+ * (utilities/silent-icons.ts). Winning it buys the icon AND its rating
39
+ * together -- one hit, one look, one fact.
40
+ *
41
+ * AND A LOUD ICON PUBLISHES ITS RATING, which is where this stops short
42
+ * of p.235 on purpose. Strictly the book charges a hit for the rating of
43
+ * ANY icon, loud or not; modelling that needs a per-fact ledger of what
44
+ * each viewer has bought about each icon, which nothing else in this
45
+ * engine keeps and which would put a roll between a player and a number
46
+ * they can already see on the shop window. The first cut gated every
47
+ * rating on `spottedBy` and the consequence was immediate: a device that
48
+ * was never silent is never spotted, so its rating read [DR ?] forever
49
+ * with no way to earn it. So the line is drawn at HIDING -- a rating you
50
+ * were never meant to see is protected, a rating the thing broadcasts is
51
+ * not -- and that is a smaller claim than p.235's, stated here rather
52
+ * than dressed up as the whole rule.
53
+ */
54
+ export function ratingKnownBy(device, viewerName) {
55
+ return !device.silent || device.spottedBy.has(viewerName);
56
+ }
57
+ /**
58
+ * The bracketed rating, or `[DR ?]` when it has not been read.
59
+ *
60
+ * A QUESTION MARK RATHER THAN NOTHING, on purpose: the icon's SHAPE is
61
+ * free (p.219 -- it looks like the thing it is), so a player should see
62
+ * that this object HAS a rating and that they have not bought it yet.
63
+ * Printing nothing would read as "this one has no rating", which is a
64
+ * different and wrong fact, and is exactly the ambiguity that made the
65
+ * camera cluster's missing DR look like an oversight.
66
+ */
67
+ export function drTag(known, rating) {
68
+ return known ? `{bold}[DR ${rating}]{/bold}` : `{bold}[DR ?]{/bold}`;
69
+ }
70
+ /** One rule for both surfaces: the grid and the AR overlay ask this, so
71
+ * a rating the street can read and a persona cannot is impossible. */
72
+ export function drTagFor(device, viewerName) {
73
+ return drTag(ratingKnownBy(device, viewerName), device.rating);
74
+ }
75
+ /** The camera cluster's rating tag. Cameras are always security kit and
76
+ * therefore always silent, so unlike a device there is no loud case:
77
+ * reading the lenses is the only way to their number. */
78
+ export function cameraDrTag(room, viewerName) {
79
+ return drTag(room.camerasSpottedBy.has(viewerName), room.cameraRating);
80
+ }
81
+ /** Trailing space for the icon lines that interpolate it mid-sentence. */
82
+ function cameraDr(room, viewerName) {
83
+ return `${cameraDrTag(room, viewerName)} `;
84
+ }
26
85
  /**
27
86
  * A DEVICE ICON, AND WHAT WORKS IT (commands/disable.ts, 2026-09-13).
28
87
  * Control Device (p.238) needs 2 marks on the device -- "disable" -- and
@@ -30,7 +89,7 @@ export function isNearDivider(line) {
30
89
  * locked (p.228), so its line says so rather than offering a way in.
31
90
  */
32
91
  export function deviceIconLine(device, actor) {
33
- const dr = `{bold}[DR ${device.rating}]{/bold}`;
92
+ const dr = drTag(ratingKnownBy(device, actor.name), device.rating);
34
93
  if (device.isOpen())
35
94
  return `▤ ${device.name} ${dr} -- its icon standing open, nothing left to hold.`;
36
95
  if (device.isBricked) {
@@ -275,10 +334,10 @@ export function gridIcons(scene, actor, room) {
275
334
  if (isWatched(room) && room.camerasSpottedBy.has(actor.name)) {
276
335
  const rooms = scene.getRooms();
277
336
  if (!camerasLive(rooms, room)) {
278
- icons.push(`◎ Camera cluster -- looped on empty hallways. Security sees NOTHING here.${canSnoopFeeds(rooms, room, actor) ? ` ("snoop" rides the feeds you own)` : ''}`);
337
+ icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- looped on empty hallways. Security sees NOTHING here.${canSnoopFeeds(rooms, room, actor) ? ` ("snoop" rides the feeds you own)` : ''}`);
279
338
  }
280
339
  else if (canSnoopFeeds(rooms, room, actor)) {
281
- icons.push(`◎ Camera cluster -- live, and answering to YOU as much as the house. ("snoop <room>" watches remotely)`);
340
+ icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- live, and answering to YOU as much as the house. ("snoop <room>" watches remotely)`);
282
341
  }
283
342
  else {
284
343
  // Seen from the grid, the cluster is worth naming as a WAY IN,
@@ -296,7 +355,7 @@ export function gridIcons(scene, actor, room) {
296
355
  // exactly one, so the reader was left to guess at a fact the
297
356
  // engine had already handed them.
298
357
  const owner = cameraMasterHost(scene.getRooms(), room);
299
- icons.push(`◎ Camera cluster -- live, feeding ${owner ? hostLabel(owner) : `the site's host`}. Loud acts here travel. (own the host to loop them -- or walk in and "tap" a lens in the flesh: slaved to the host means a key to the host)`);
358
+ icons.push(`◎ Camera cluster ${cameraDr(room, actor.name)}-- live, feeding ${owner ? hostLabel(owner) : `the site's host`}. Loud acts here travel. (own the host to loop them -- or walk in and "tap" a lens in the flesh: slaved to the host means a key to the host)`);
300
359
  }
301
360
  }
302
361
  // THE DIVIDER (eJANPpm7qCaCAJBJw). Only when there is a host to be
@@ -570,14 +629,28 @@ export function gridSculpt(actor, room) {
570
629
  * the dimmer their icons"). `rooms` absent means the caller has only
571
630
  * the one room, and the block reads as it always did.
572
631
  */
573
- export function gridIconBlock(scene, actor, room, rooms) {
632
+ export function gridIconBlock(scene, actor, room, rooms, justFound) {
574
633
  const isHostLine = (i) => i.startsWith('●') || i.startsWith('■');
634
+ // WHAT THIS LOOK JUST TURNED UP, tagged where it is listed
635
+ // (eJANPpm7qCaCAJBJw: "I still see the Service Keypad and Camera
636
+ // cluster"). An icon a sweep has just won is otherwise identical on
637
+ // the page to one that was never hidden -- and on every LATER look
638
+ // there is no sweep line at all, because there is nothing left to
639
+ // roll against, so without this the moment of finding something is
640
+ // invisible and the whole mechanic reads as not having run.
641
+ const mark = (icon) => {
642
+ if (!justFound?.length)
643
+ return icon;
644
+ return justFound.some(n => icon.includes(n))
645
+ ? `${icon} {bold}<- just spotted, was running silent{/bold}`
646
+ : icon;
647
+ };
575
648
  if (actor.insideHost || !rooms) {
576
649
  const icons = gridIcons(scene, actor, room);
577
650
  if (icons.length === 0) {
578
651
  return `{light-blue-fg}Thin grid out here -- background noise, nothing worth cracking.{/light-blue-fg}`;
579
652
  }
580
- return `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${icons.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`;
653
+ return `{light-blue-fg}ICONS IN REACH:{/light-blue-fg}\n${icons.map(i => ` {light-blue-fg}${mark(i)}{/light-blue-fg}`).join('\n')}`;
581
654
  }
582
655
  const out = [];
583
656
  const hosts = hostsInReach(rooms, actor);
@@ -629,12 +702,12 @@ export function gridIconBlock(scene, actor, room, rooms) {
629
702
  return line;
630
703
  // Box-drawing, so the last child closes the branch and the eye can
631
704
  // see where the WAN ends.
632
- const kids = slaved.map((i, idx) => ` {light-blue-fg}${idx === slaved.length - 1 ? '└─' : '├─'} ${i}{/light-blue-fg}`);
705
+ const kids = slaved.map((i, idx) => ` {light-blue-fg}${idx === slaved.length - 1 ? '└─' : '├─'} ${mark(i)}{/light-blue-fg}`);
633
706
  return [line, ...kids].join('\n');
634
707
  }).join('\n')}`
635
708
  : `{light-blue-fg}Nothing overhead -- no host stands over this district.{/light-blue-fg}`);
636
709
  out.push(loose.length > 0
637
- ? `{light-blue-fg}ICONS IN REACH${slaved.length > 0 ? ' -- answering to nobody up there' : ''}:{/light-blue-fg}\n${loose.map(i => ` {light-blue-fg}${i}{/light-blue-fg}`).join('\n')}`
710
+ ? `{light-blue-fg}ICONS IN REACH${slaved.length > 0 ? ' -- answering to nobody up there' : ''}:{/light-blue-fg}\n${loose.map(i => ` {light-blue-fg}${mark(i)}{/light-blue-fg}`).join('\n')}`
638
711
  : slaved.length > 0
639
712
  ? `{light-blue-fg}Nothing else near -- everything around you hangs off the host.{/light-blue-fg}`
640
713
  : `{light-blue-fg}Nothing near -- background noise around your signal.{/light-blue-fg}`);
@@ -0,0 +1,62 @@
1
+ import { buildReproScene, reproArchetypeName } from '../factories/repro-scene.js';
2
+ import { saveForArchetype } from '../headless-harness.js';
3
+ import { REPRO_ACTOR, reproFixtureOf } from '../types/repro.js';
4
+ /**
5
+ * The scene and the runner a bench starts with.
6
+ */
7
+ export function buildReproBoot(repro) {
8
+ const fixture = reproFixtureOf(repro);
9
+ const build = buildReproScene(fixture);
10
+ const archetype = reproArchetypeName(fixture);
11
+ return {
12
+ fixture,
13
+ build,
14
+ seed: build.seed,
15
+ archetype,
16
+ save: saveForArchetype(REPRO_ACTOR, archetype),
17
+ warnings: build.warnings,
18
+ };
19
+ }
20
+ /**
21
+ * THE KNOBS THE SEED CANNOT CARRY, applied to a booted session.
22
+ *
23
+ * Call this after the session is up and before the first step (or, on
24
+ * the site, before the first status push) -- the point of both knobs is
25
+ * that the bench OPENS on a runner already in that state, so a reporter
26
+ * reads the condition on their first look rather than watching it be
27
+ * applied.
28
+ *
29
+ * Silent when the fixture asks for neither, and silent when the actor is
30
+ * missing: a bench whose runner did not boot has a larger problem than
31
+ * its damage boxes, and it will say so in the transcript.
32
+ */
33
+ export async function applyReproFixtureState(game, fixture, actorName = REPRO_ACTOR) {
34
+ // START WOUNDED (fixture.wounded): the boxes go on before the first
35
+ // step, so the transcript opens on a runner who already needs the
36
+ // verb under test.
37
+ if (fixture.wounded) {
38
+ const runner = game.scene.getPlayerByName(actorName);
39
+ if (runner) {
40
+ if (fixture.wounded.physical)
41
+ runner.takeDamage(fixture.wounded.physical);
42
+ if (fixture.wounded.stun)
43
+ runner.takeStun(fixture.wounded.stun);
44
+ }
45
+ }
46
+ // NUYEN (fixture.nuyen): a credstick in the pack, because money lives
47
+ // on a carrier item (Player.currency sums them) and the pregen has none.
48
+ if (fixture.nuyen && fixture.nuyen > 0) {
49
+ const runner = game.scene.getPlayerByName(actorName);
50
+ if (runner) {
51
+ const { ItemFactory } = await import('../factories/item-factory.js');
52
+ const stick = await new ItemFactory().createItemFromJson({
53
+ name: 'Certified Credstick', description: 'Cash that does not ask questions.',
54
+ size: 'Tiny', category: 'Credstick', shape: 'Slab', color: 'Black', texture: 'Plastic',
55
+ rating: 'Common', weight: 0.1, transferable: true,
56
+ });
57
+ stick.addCurrency(fixture.nuyen);
58
+ runner.addInventory(stick);
59
+ }
60
+ }
61
+ }
62
+ //# sourceMappingURL=repro-boot.js.map
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import { fetchReviewQueue } from './backlog.js';
3
+ import { REPRO_ACTOR } from '../types/repro.js';
3
4
  /**
4
5
  * Resolve `<id>` or `<n>` against a FRESH review queue.
5
6
  *
@@ -157,8 +158,14 @@ export function readReproFile(candidate) {
157
158
  * since the last step and print them under the output they explain.
158
159
  */
159
160
  /** The bench's actor. One name, used for the seat and for the
160
- * visibility filter below. */
161
- export const REPRO_ACTOR = 'Tester';
161
+ * visibility filter below.
162
+ *
163
+ * Defined in types/repro.ts and re-exported here so the many callers
164
+ * that import it from the launcher keep working. It had to move: the
165
+ * site needs the same constant, and a module that reaches it through
166
+ * anything importing the scene builder would drag the game graph back
167
+ * across this file's lazy boundary. See the note on the constant. */
168
+ export { REPRO_ACTOR };
162
169
  const stripTags = (s) => s.replace(/\{\/?[a-z-]+\}/g, '');
163
170
  /**
164
171
  * ONE STEP OF TRANSCRIPT, pure: the command's return string, then what
@@ -221,49 +228,23 @@ export function renderStepLines(step, text, fresh, actor = REPRO_ACTOR) {
221
228
  return lines;
222
229
  }
223
230
  export async function dryRunRepro(repro) {
224
- const [{ buildReproScene, reproArchetypeName }, { bootSession, saveForArchetype }] = await Promise.all([
225
- import('../factories/repro-scene.js'),
231
+ // The scene, the runner and the post-boot knobs all come from
232
+ // repro-boot, which is the ONE definition of what a bench boots as --
233
+ // shared with maka-cli.com's live bench so the transcript below and
234
+ // the session a reporter types into cannot describe different
235
+ // scenarios. See that module's header.
236
+ const [{ buildReproBoot, applyReproFixtureState }, { bootSession }] = await Promise.all([
237
+ import('./repro-boot.js'),
226
238
  import('../headless-harness.js'),
227
239
  ]);
228
- const fixture = { ...(repro.fixture ?? {}) };
229
- // The reader's framing doubles as the bench's story when the fixture
230
- // has none of its own -- one sentence, written once.
231
- if (!fixture.premise && repro.premise)
232
- fixture.premise = repro.premise;
233
- const built = buildReproScene(fixture);
240
+ const built = buildReproBoot(repro);
234
241
  const events = [];
235
242
  const session = await bootSession({
236
243
  seed: built.seed,
237
- players: [saveForArchetype(REPRO_ACTOR, reproArchetypeName(fixture))],
244
+ players: [built.save],
238
245
  events,
239
246
  });
240
- // START WOUNDED (fixture.wounded): the boxes go on before the first
241
- // step, so the transcript opens on a runner who already needs the
242
- // verb under test.
243
- if (fixture.wounded) {
244
- const runner = session.game.scene.getPlayerByName(REPRO_ACTOR);
245
- if (runner) {
246
- if (fixture.wounded.physical)
247
- runner.takeDamage(fixture.wounded.physical);
248
- if (fixture.wounded.stun)
249
- runner.takeStun(fixture.wounded.stun);
250
- }
251
- }
252
- // NUYEN (fixture.nuyen): a credstick in the pack, because money lives
253
- // on a carrier item (Player.currency sums them) and the pregen has none.
254
- if (fixture.nuyen && fixture.nuyen > 0) {
255
- const runner = session.game.scene.getPlayerByName(REPRO_ACTOR);
256
- if (runner) {
257
- const { ItemFactory } = await import('../factories/item-factory.js');
258
- const stick = await new ItemFactory().createItemFromJson({
259
- name: 'Certified Credstick', description: 'Cash that does not ask questions.',
260
- size: 'Tiny', category: 'Credstick', shape: 'Slab', color: 'Black', texture: 'Plastic',
261
- rating: 'Common', weight: 0.1, transferable: true,
262
- });
263
- stick.addCurrency(fixture.nuyen);
264
- runner.addInventory(stick);
265
- }
266
- }
247
+ await applyReproFixtureState(session.game, built.fixture);
267
248
  const lines = [];
268
249
  // THE FIRST PAINT IS NOT PART OF STEP ONE. bootSession pushes every
269
250
  // player's panels the moment the session comes up (B7NNNesy5gvRSbZpp),
@@ -1,7 +1,6 @@
1
1
  import { rollPool, formatRoll } from './dice.js';
2
2
  import { deviceDefensePool } from './matrix-intrusion.js';
3
3
  import { isWatched } from './surveillance.js';
4
- import { DEFAULT_DEVICE_RATING } from '../models/device.js';
5
4
  function hiddenDevice(device) {
6
5
  return {
7
6
  name: device.name,
@@ -34,25 +33,27 @@ export function hiddenIconsIn(scene, room, viewer) {
34
33
  .map(hiddenDevice);
35
34
  // THE CAMERAS, which p.217 names FIRST. They are not a Device -- the
36
35
  // cluster is derived from the room (surveillance.ts isWatched) -- so
37
- // they hide on the room's own set, and they hide behind their OWN
38
- // rating: an ordinary fixture, which canon prices at 2 (p.356).
36
+ // the rating rides the ROOM (Room.cameraRating), seeded per site.
39
37
  //
40
38
  // THE FIRST CUT GAVE THEM THEIR MASTER HOST'S RATING and that was
41
39
  // wrong twice over. Canon first: p.233's slaving rule is about what a
42
- // slave DEFENDS AN ATTACK with -- "whenever a slaved device is called
43
- // on to make a defense test" -- and hiding is not an attack being
44
- // defended. Borrowing the host's numbers here was an extrapolation
45
- // with nothing under it, which is exactly what this project's rule
46
- // against inventing canon is for. And then arithmetic: a rating-8
47
- // host gave its lenses 16 dice against a sweep the Data Processing
48
- // limit caps near 5, so a camera on any corporate host was not hard
49
- // to find, it was impossible. Measured before shipping the fix --
50
- // zero finds in twelve looks.
40
+ // slave DEFENDS AN ATTACK with, and hiding is not an attack being
41
+ // defended. Then arithmetic: a rating-8 host gave its lenses 16 dice
42
+ // against a sweep the Data Processing limit caps near 5, so a camera
43
+ // on any corporate host was not hard to find, it was impossible --
44
+ // measured at zero finds in twelve looks.
45
+ //
46
+ // THE SECOND CUT hard-coded a 4 in here, which fixed the arithmetic
47
+ // and left the engine holding THREE answers to one question: this
48
+ // hid behind 4, commands/disable.ts defended a Data Spike with the
49
+ // HOST's pool, and the icon line showed no rating at all. A player
50
+ // could not have reconciled them because two of the three were never
51
+ // printed. One seeded number feeds all three now, so what the player
52
+ // reads is what it rolls (reporter's ruling: per-site, not global).
51
53
  if (isWatched(room) && !room.camerasSpottedBy.has(viewer.name)) {
52
- const rating = DEFAULT_DEVICE_RATING.lock ?? 2;
53
54
  out.push({
54
55
  name: 'Camera cluster',
55
- hide: { pool: Math.max(0, rating * 2), label: 'Device Rating' },
56
+ hide: { pool: Math.max(0, room.cameraRating * 2), label: 'Device Rating + Firewall' },
56
57
  reveal: (v) => { room.camerasSpottedBy.add(v); },
57
58
  });
58
59
  }
@@ -98,28 +99,44 @@ export function spotHiddenIcons(scene, actor, room) {
98
99
  + actor.matrixActionPenalty + actor.woundModifier - actor.sustainingPenalty);
99
100
  const roll = rollPool(pool, actor.matrixAttribute('dataProcessing'), { gremlins: actor.deckGremlins });
100
101
  const found = [];
101
- const missed = [];
102
+ let missed = 0;
102
103
  const against = [];
103
104
  for (const icon of hidden) {
104
105
  const resist = rollPool(icon.hide.pool);
105
- against.push({ name: icon.name, label: icon.hide.label, roll: resist });
106
106
  // Ties go to the hider: "on a tie or more hits by the defender, it
107
107
  // stays hidden and out of reach" (p.236).
108
108
  if (roll.hits > resist.hits) {
109
109
  icon.reveal(actor.name);
110
110
  found.push(icon.name);
111
+ against.push({ name: icon.name, label: icon.hide.label, roll: resist });
111
112
  }
112
113
  else {
113
- missed.push(icon.name);
114
+ missed += 1;
115
+ // THE NAME DOES NOT LEAVE THIS BRANCH. Only the dice do -- see
116
+ // ISpotOutcome.missed. Attaching the name here "just for the
117
+ // mechanics line" is exactly how it leaked the first time.
118
+ against.push({ roll: resist });
114
119
  }
115
120
  }
116
121
  return { found, missed, roll, against };
117
122
  }
118
- /** The mechanics lines for a sweep, for a screen that shows them. */
123
+ /**
124
+ * The mechanics lines for a sweep.
125
+ *
126
+ * AN ICON YOU DID NOT BEAT IS NOT NAMED HERE (user ruling 2026-09-16).
127
+ * Your own roll is always shown -- it is yours. What it was rolled
128
+ * against is shown only where you won it: a loss prints its dice and
129
+ * calls the roller "something", because naming it would hand over for
130
+ * free precisely what the opposed test exists to charge for (p.235:
131
+ * the type of an icon and its device rating are things a Matrix
132
+ * Perception HIT buys).
133
+ */
119
134
  export function spotMechanics(outcome) {
120
135
  return [
121
136
  `Matrix Perception, silent icons (Computer + Intuition [Data Processing]): ${formatRoll(outcome.roll)}`,
122
- ...outcome.against.map(a => ` ${a.name} hides (${a.label}): ${formatRoll(a.roll)}`),
137
+ ...outcome.against.map(a => (a.name
138
+ ? ` ${a.name} hides (${a.label}): ${formatRoll(a.roll)}`
139
+ : ` something holds its cover: ${formatRoll(a.roll)}`)),
123
140
  ];
124
141
  }
125
142
  /**
@@ -135,10 +152,14 @@ export function spotLine(outcome) {
135
152
  if (outcome.found.length > 0) {
136
153
  lines.push(`You pick ${outcome.found.length === 1 ? 'an icon' : `${outcome.found.length} icons`} out of the noise -- ${outcome.found.join(', ')}, running silent and not quiet enough.`);
137
154
  }
138
- if (outcome.missed.length > 0) {
139
- lines.push(outcome.missed.length === 1
155
+ if (outcome.missed > 0) {
156
+ // A COUNT AND NOTHING ELSE. You learn that the room is holding
157
+ // something back -- which is what a hit buys under p.235-236 -- and
158
+ // not what it is. `missed` is a number precisely so this line cannot
159
+ // grow a name later.
160
+ lines.push(outcome.missed === 1
140
161
  ? `Something else here is running silent and you cannot pin it -- a shape in the static that stops when you look at it. (Look again; each sweep is its own Matrix Perception, p.241.)`
141
- : `${outcome.missed.length} more icons are running silent here, and none of them will hold still. (Look again; each sweep is its own Matrix Perception, p.241.)`);
162
+ : `${outcome.missed} more icons are running silent here, and none of them will hold still. (Look again; each sweep is its own Matrix Perception, p.241.)`);
142
163
  }
143
164
  return lines.join('\n');
144
165
  }
@@ -6,6 +6,10 @@ import { Actions } from '../../tools/actions/actions.class.js';
6
6
  import { Log } from '../../tools/log/log.class.js';
7
7
  import { AI } from '../../tools/ai/ai.class.js';
8
8
  import { spawnSelf } from '../../tools/self-spawn/self-spawn.js';
9
+ // A value import, not a type-only one, for reproFixtureOf -- safe
10
+ // against this file's "no game graph on `maka --help`" rule because
11
+ // types/repro.ts reaches only two leaf enum modules.
12
+ import { reproFixtureOf } from './sideQuest/types/repro.js';
9
13
  /**
10
14
  * SPAWN THE GAME AND BE ITS JANITOR.
11
15
  *
@@ -77,9 +81,12 @@ export async function spawnGame(launchFilePath, logFilePath) {
77
81
  */
78
82
  async function launchReproBench(repro, item, makaDir) {
79
83
  const { buildReproScene, reproArchetypeName } = await import('./sideQuest/factories/repro-scene.js');
80
- const fixture = { ...(repro.fixture ?? {}) };
81
- if (!fixture.premise && repro.premise)
82
- fixture.premise = repro.premise;
84
+ // The one premise rule, shared with the dry run and the web bench --
85
+ // see reproFixtureOf. This path stops at the scene: the game rolls the
86
+ // character itself from `archetype` below, so it deliberately does NOT
87
+ // go through repro-boot, which would pull the headless harness (and
88
+ // with it game.ts) into the PARENT process that only spawns the child.
89
+ const fixture = reproFixtureOf(repro);
83
90
  let built;
84
91
  try {
85
92
  built = buildReproScene(fixture);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.226.0",
3
+ "version": "5.228.0",
4
4
  "type": "module",
5
5
  "summary": "A command line tool for scaffolding Meteor 3.x applications using either React.",
6
6
  "description": "A command line tool for scaffolding Meteor 3.x applications using React.",