@maka/maka-cli 5.186.0 → 5.188.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.186.0",
3
+ "version": "5.188.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.",
@@ -556,6 +556,31 @@ export class HackCommand extends BypassCommand {
556
556
  this.logger.meta(`${defense.label} (DR ${device.rating}): ${formatRoll(defenseRoll)} -- net ${netHits >= 0 ? '+' : ''}${netHits}`);
557
557
  }
558
558
  const godLines = this.godBill(overwatchFromDefense(defenseRoll.hits), `device intrusion (${mode})`);
559
+ // WHY THE KEYS YOU ALREADY HOLD DID NOT OPEN THIS (u8u2koi3THQM9dzSz:
560
+ // "I _OWN_ the host with 3 marks, I should be able to hack the
561
+ // maglock, or simply disable it").
562
+ //
563
+ // Checked against the books rather than the report's own reading:
564
+ // marks share ONE WAY. "If you get a mark on a slave you also get a
565
+ // mark on the master ... This doesn't work both ways" (p.233). Three
566
+ // marks on the host are three marks on the HOST; the lock hanging off
567
+ // it is still its own icon with its own monitor, and Control Device
568
+ // (p.238) counts marks on the thing being controlled. So the engine
569
+ // was right to make them roll -- and said nothing at all about why,
570
+ // which is the part that earned the report. A rule the player cannot
571
+ // see is indistinguishable from a bug.
572
+ //
573
+ // The line also names the move that DOES cash those marks in: inside
574
+ // the host you are directly connected to everything on its WAN, and
575
+ // a directly-connected slave "cannot use its master's ratings to
576
+ // defend itself" (p.233) -- which is exactly the pool that just beat
577
+ // them. Only said when they actually hold host marks and are still
578
+ // outside; from in here it would be telling them to walk through the
579
+ // door they are standing in.
580
+ const hostMarksHeld = viaWan && host ? host.hostMarksBy.get(actor.name) ?? 0 : 0;
581
+ const wanNote = hostMarksHeld > 0
582
+ ? hint(` (Your ${hostMarksHeld} mark${hostMarksHeld === 1 ? '' : 's'} on ${hostLabel(host)} buy nothing against the lock itself -- marks run slave-to-master, never master-to-slave (p.233). "enter ${hostLabel(host)}": from inside its WAN you are directly connected, and ${device.name} has to defend on DR ${device.rating} alone.)`)
583
+ : '';
559
584
  if (netHits <= 0) {
560
585
  if (mode === 'sleaze') {
561
586
  // A blown Sleaze is NOTICED (p.236) -- and a slaved device that
@@ -565,14 +590,14 @@ export class HackCommand extends BypassCommand {
565
590
  host.hostAlert = true;
566
591
  this.scene.addWorldEvent(`${device.name} flags a bad handshake -- ${hostLabel(host, { capital: true })} is awake.`);
567
592
  this.scene.updateStatus();
568
- return [`${device.name} refuses the key and SAYS SO -- the node it answers to knows something is in here.`, ...godLines].join('\n');
593
+ return [`${device.name} refuses the key and SAYS SO -- the node it answers to knows something is in here.${wanNote}`, ...godLines].join('\n');
569
594
  }
570
595
  // A loose device has no host to wake; its owner still gets the
571
596
  // mark on you a blown Sleaze hands over (p.240).
572
597
  this.scene.updateStatus();
573
598
  return [`${device.name} refuses the key and SAYS SO -- whoever owns it has your icon on file now.`, ...godLines].join('\n');
574
599
  }
575
- return [`${device.name} holds against the smash -- its firewall never even flexes. Nobody upstairs noticed.`, ...godLines].join('\n');
600
+ return [`${device.name} holds against the smash -- its firewall never even flexes. Nobody upstairs noticed.${wanNote}`, ...godLines].join('\n');
576
601
  }
577
602
  const now = Math.min(MAX_MARKS, held + declared);
578
603
  device.marksBy.set(actor.name, now);
@@ -733,6 +733,15 @@ For EVERY item in the skeleton, expand it into a full item ("name" and "category
733
733
  Clue requirements (mechanically validated -- the exact phrase must appear in "details"):
734
734
  ${clueSpecs || '- (no clue items in this scene)'}
735
735
 
736
+ THE ONLY PRIZE IN THIS WORLD is "${skeleton.winCondition?.item ?? ''}" -- the item
737
+ "winCondition" is for. No other item may present itself as the thing the client wants: not
738
+ "it's what ${skeleton.winCondition?.toNpc || skeleton.client || 'the Johnson'} is looking
739
+ for", not "this is what the job is about", not a second set of goods that reads like the
740
+ objective wearing a different name. The engine stars exactly one item and pays out on
741
+ exactly one; a second item that CLAIMS the role sends the runner home with the wrong thing
742
+ in their pack, believing they blew the job they just finished. Loot is loot -- say what it
743
+ IS, never what it is worth to the client.
744
+
736
745
  THE ONLY CODES IN THIS WORLD are the clue phrases listed just above. No other item may
737
746
  carry a code-shaped string -- no serial numbers, no model numbers, no unit or locker or
738
747
  apartment numbers, no "access code" on a scrap of paper that isn't a listed clue. A player
@@ -2046,6 +2055,112 @@ export function ensureCredentialKindAgrees(details) {
2046
2055
  }
2047
2056
  return notes;
2048
2057
  }
2058
+ /**
2059
+ * ONE PRIZE, AND THE PROSE HAS TO AGREE (aSpARWJXXieMAL4eM, 2026-09-14:
2060
+ * "the item marked with the star as the mission objective was 'The
2061
+ * Scout's Contact Sheet'. But the mission was clearly about camera
2062
+ * data ... When I returned to the Hub, I had 'Developed Negatives',
2063
+ * which state it's what Mr. Leger is looking for. It's like the two
2064
+ * items got mis labeld as to what was important.").
2065
+ *
2066
+ * Nothing was mislabelled in the STATE. winCondition.item was the
2067
+ * contact sheet, the star followed it (planes.ts objectiveMark), the
2068
+ * completion check accepted it, and the payout landed. What went wrong
2069
+ * is that the items pass wrote a SECOND item's flavour as though that
2070
+ * one were the job -- so the run had one prize by the engine's reckoning
2071
+ * and a different one by its own prose, and the player believed the
2072
+ * prose. That is worse than a wrong star: they finished the job
2073
+ * convinced they had failed it.
2074
+ *
2075
+ * Same family as payoutSummary and heatSummary in npc.ts -- the model
2076
+ * inventing a fact the engine already owns -- and the same answer:
2077
+ * state the fact in the prompt (buildItemsPrompt, "THE ONLY PRIZE IN
2078
+ * THIS WORLD"), and enforce it here so a draft that ignores the rule
2079
+ * costs nothing.
2080
+ *
2081
+ * SURGERY, narrowly, and this file's own preference is against it:
2082
+ * ensureCredentialKindAgrees corrects "by ADDING a plain sentence rather
2083
+ * than rewriting generated prose". That works when the generated line is
2084
+ * merely incomplete. It cannot work here, because the line is FALSE --
2085
+ * appending "but the client doesn't want this" under "this is what the
2086
+ * client wants" leaves prose that argues with itself, which is exactly
2087
+ * the confusion the report describes. So the false sentence comes out,
2088
+ * whole, and everything around it is left byte-identical.
2089
+ *
2090
+ * THE BAR IS THREE THINGS IN ONE SENTENCE, because two is not enough to
2091
+ * tell a claim from colour: someone the job is ABOUT (the client by
2092
+ * name, or a generic for them), a verb of WANTING, and a reference back
2093
+ * to THIS item. "Ledger drinks alone and wants nothing from anybody"
2094
+ * keeps all three words apart and survives; "it's what Mr. Ledger is
2095
+ * looking for" does not.
2096
+ */
2097
+ const PRIZE_WANTING = /\b(look(?:s|ing|ed)?\s+for|after\s+(?:this|it|these|them)|want(?:s|ed|ing)?|need(?:s|ed|ing)?|ask(?:s|ed|ing)?\s+for|pay(?:s|ing)?\s+for|hir(?:ed|ing)\s+\w+\s+for|sent\s+\w+\s+for|came\s+for|is\s+the\s+(?:job|prize|objective|target))\b/i;
2098
+ const PRIZE_SELF = /\b(this|these|those|it|its|it's|the\s+one)\b/i;
2099
+ const PRIZE_GENERIC_CLIENT = /\b(the\s+client|the\s+johnson|mr\.?\s+johnson|your\s+employer|the\s+employer|the\s+buyer|the\s+job|the\s+run|the\s+contract)\b/i;
2100
+ /**
2101
+ * Sentence-ish split that keeps the terminator with its sentence.
2102
+ *
2103
+ * IT DOES HAVE TO HANDLE "Mr." -- this comment first said it didn't,
2104
+ * and the first test written against it failed. Every client in this
2105
+ * game is "a Mr. Johnson" (canon p.27-28), so the one sentence this
2106
+ * function exists to read -- "It's what Mr. Ledger is looking for." --
2107
+ * is precisely the one a naive split cuts in half, into two fragments
2108
+ * neither of which claims anything. The titles below are the ones a
2109
+ * Shadowrun scene actually writes; a single capital initial ("J.
2110
+ * Ledger") goes with them.
2111
+ */
2112
+ const SENTENCE_BREAK = /(?<!\b(?:Mr|Mrs|Ms|Dr|Sr|Jr|St|Prof|Rev|Inc|Ltd|Co|vs|No|[A-Z])\.)(?<=[.!?])\s+/;
2113
+ function prizeSentences(text) {
2114
+ return text.split(SENTENCE_BREAK).filter(s => s.length > 0);
2115
+ }
2116
+ function claimsThePrize(sentence, clientNames) {
2117
+ if (!PRIZE_WANTING.test(sentence) || !PRIZE_SELF.test(sentence))
2118
+ return false;
2119
+ const named = clientNames.some(n => n.length > 0 && sentence.toLowerCase().includes(n.toLowerCase()));
2120
+ return named || PRIZE_GENERIC_CLIENT.test(sentence);
2121
+ }
2122
+ export function ensureOnePrize(skeleton, details) {
2123
+ const notes = [];
2124
+ const prize = (skeleton.winCondition?.item ?? '').trim().toLowerCase();
2125
+ if (prize.length === 0)
2126
+ return notes;
2127
+ // The people a claim can be made on behalf of: the client who holds
2128
+ // the meet, and whoever the goods are owed to (usually the same
2129
+ // person, sometimes not -- a courier job delivers to a third party).
2130
+ // Last names alone are how these lines actually read ("Ledger takes
2131
+ // the sleeve"), so each name contributes its words too.
2132
+ const clientNames = [skeleton.client ?? '', skeleton.winCondition?.toNpc ?? '']
2133
+ .flatMap(n => [n, ...n.split(/\s+/)])
2134
+ .map(n => n.replace(/[^\w\s'-]/g, '').trim())
2135
+ .filter(n => n.length >= 4);
2136
+ for (const item of details.items ?? []) {
2137
+ if ((item.name ?? '').trim().toLowerCase() === prize)
2138
+ continue;
2139
+ for (const field of ['description', 'details']) {
2140
+ const text = (item[field] ?? '').trim();
2141
+ if (text.length === 0)
2142
+ continue;
2143
+ const kept = prizeSentences(text).filter(s => !claimsThePrize(s, clientNames));
2144
+ if (kept.length === prizeSentences(text).length)
2145
+ continue;
2146
+ const rebuilt = kept.join(' ').trim();
2147
+ // A description is always-visible flavour and cannot be blank --
2148
+ // an item with no line at all reads as a generator failure. A
2149
+ // "details" that empties out simply goes away, which is what an
2150
+ // item with nothing more to say should do.
2151
+ if (field === 'description') {
2152
+ item.description = rebuilt.length > 0
2153
+ ? rebuilt
2154
+ : `Someone's property once, and worth carrying if nobody's looking.`;
2155
+ }
2156
+ else {
2157
+ item.details = rebuilt.length > 0 ? rebuilt : undefined;
2158
+ }
2159
+ notes.push(`normalized: item "${item.name}" claimed in its ${field} to be what the job is for; "${skeleton.winCondition.item}" is, so the claim was cut.`);
2160
+ }
2161
+ }
2162
+ return notes;
2163
+ }
2049
2164
  /**
2050
2165
  * RESOLVE A NAME ONE LIST WROTE AGAINST THE LIST THAT OWNS IT.
2051
2166
  *
@@ -2110,6 +2225,7 @@ export function assembleScene(skeleton, details, playerName) {
2110
2225
  // The answer has to be legible before anything is assembled from it.
2111
2226
  ensureClueDetails(skeleton, details);
2112
2227
  ensureCredentialKindAgrees(details);
2228
+ ensureOnePrize(skeleton, details);
2113
2229
  const roomDetail = new Map(details.rooms.map(r => [r.name, r]));
2114
2230
  const npcDetail = new Map(details.npcs.map(n => [n.name, n]));
2115
2231
  const deviceDetail = new Map(details.devices.map(d => [d.name, d]));
@@ -13,6 +13,7 @@ import { Logger } from '../utilities/logger.js';
13
13
  import { migrateLegacySeed } from './seed-migration.js';
14
14
  import { Device } from '../models/device.js';
15
15
  import { fuzzyPickName } from '../utilities/fuzzy-match.js';
16
+ import { adoptSeededIce } from '../utilities/ic-actors.js';
16
17
  /**
17
18
  * How many AI enrichment calls may be in flight at once.
18
19
  *
@@ -252,6 +253,17 @@ Key Locations: ${json.rooms.map(r => r.name).join(', ')}
252
253
  // architecture nobody wrote.
253
254
  if (isIce && npc.currentLocation?.hasNode) {
254
255
  npc.insideHost = npc.currentLocation;
256
+ // ...AND THE HOST HAS TO KNOW IT IS RUNNING IT (mPCh2FJykPsyJKmcc's
257
+ // bench, 2026-09-14). Standing inside the node was only half the
258
+ // adoption: nothing registered the construct with the Host, so it
259
+ // was ice by every reading except the one the host itself uses.
260
+ // The host then launched its own twin on the first Combat Turn
261
+ // (p.247 forbids two of a type), the construct swung with an
262
+ // Attack of 0 because "IC uses the Matrix attributes of its host"
263
+ // is read off icHost, and the grid view listed it twice. See
264
+ // adoptSeededIce for the three-for-one.
265
+ if (npc.currentLocation.host)
266
+ adoptSeededIce(npc, npc.currentLocation.host);
255
267
  }
256
268
  npc.scene = scene;
257
269
  builtNpcs.push(npc);
@@ -458,6 +458,28 @@ export function splitSpokenLines(lines) {
458
458
  rebuilt.push(`[COMMAND] say ${bracketed[1]}`);
459
459
  continue;
460
460
  }
461
+ // ...AND SO IS ONE FILED UNDER A TAG THE MODEL INVENTED
462
+ // (zQTWGJFWDXoLtiJX6, 2026-09-13: Whisper's whole sales line
463
+ // arrived as `[TALK] say Depends what you're buying or
464
+ // confessing. The credstick's hot and priced like it...` and
465
+ // printed into the world log verbatim, marker and all -- the
466
+ // reporter first read it as his OWN typing echoed back).
467
+ //
468
+ // Third sighting of one root: the model reaches for the bracket
469
+ // convention and gets the WORD inside it wrong. [say] was the
470
+ // second; this is the general case, and generalising beats
471
+ // adding [TALK] to a list that would need [DIALOGUE], [ACTION]
472
+ // and whatever the next model invents.
473
+ //
474
+ // The bound that holds every repair in this function holds here
475
+ // too: the VERB after the bracket must be `say`. `[go] north`
476
+ // stays exactly what it was -- only `say` carries free prose, so
477
+ // only `say` can be promoted without risking a silent act.
478
+ const tagged = t.match(/^\[\s*[A-Za-z][A-Za-z ._-]*\s*\]\s*say\s+(\S.*)$/i);
479
+ if (tagged) {
480
+ rebuilt.push(`[COMMAND] say ${tagged[1]}`);
481
+ continue;
482
+ }
461
483
  // ...AND A LINE THAT IS NOTHING BUT SPEECH MARKS IS SPEECH
462
484
  // (playtest 2026-08-27, reported twice in one session: 'the
463
485
  // "Take a seat..." comment isn't showing as he said it. It's in
@@ -1,5 +1,6 @@
1
1
  import { hostConcealedFrom } from './jackpoint.js';
2
2
  import { matchesHostName, hostLabel } from './grid-names.js';
3
+ import { fuzzyPickName } from './fuzzy-match.js';
3
4
  /**
4
5
  * THE GRID HAS NO STREETS (user canon check 2026-09-02; SR5 pp.217-218,
5
6
  * p.246). Jacked in and outside a host, a persona is on its grid: "a
@@ -44,6 +45,28 @@ export function hostOver(room) {
44
45
  * anywhere on the grid. Bare "host" (or nothing) means the one over the
45
46
  * persona's vicinity, else nothing -- with several overhead, a bare word
46
47
  * cannot pick.
48
+ *
49
+ * THE ALIAS LIST IS EXACT, AND EXACT IS NOT ENOUGH (qHwKnqEGrJFn3tmF7).
50
+ * grid-names.ts made the printer and the resolver agree, which fixed
51
+ * the lie -- but it left the player typing the label back BYTE FOR
52
+ * BYTE. A run whose room was called "The Grinning Idol -- Back Room"
53
+ * refused `mark Back Room host` and accepted only
54
+ * `mark The Grinning Idol -- Back Room host`; the reporter's word for
55
+ * it was "ridiculous", and they are right. A name you can read off the
56
+ * screen but not shorten is a name you have to transcribe.
57
+ *
58
+ * So: aliases first, unchanged -- an exact hit still resolves exactly,
59
+ * and the vicinity still wins ties. Only when nothing answers does the
60
+ * shared fuzzy matcher get a turn, over the names the map actually
61
+ * printed. It is the same resolver items and NPCs go through, with the
62
+ * same discipline: an AMBIGUOUS guess misses rather than picking a host
63
+ * at random, which matters more here than anywhere -- cracking the
64
+ * wrong node spends the action, wakes the wrong ice and cannot be
65
+ * taken back.
66
+ *
67
+ * The trailing "host"/"node" comes off first: it is part of the LABEL
68
+ * ("the Back Room host"), never part of the room's name, and leaving it
69
+ * on drags every score down by a word.
47
70
  */
48
71
  export function resolveHostInReach(rooms, actor, query) {
49
72
  const q = query.trim().toLowerCase();
@@ -56,7 +79,14 @@ export function resolveHostInReach(rooms, actor, query) {
56
79
  const near = hostOver(gridVicinity(actor));
57
80
  if (near && matchesHostName(q, near))
58
81
  return near;
59
- return hosts.find(r => matchesHostName(q, r));
82
+ const exact = hosts.find(r => matchesHostName(q, r));
83
+ if (exact)
84
+ return exact;
85
+ const bare = q.replace(/\s*\b(hosts?|nodes?)\s*$/, '').trim();
86
+ if (bare.length === 0)
87
+ return undefined;
88
+ const picked = fuzzyPickName(bare, hosts.map(r => r.name));
89
+ return picked ? hosts.find(r => r.name === picked) : undefined;
60
90
  }
61
91
  /** Noise between the persona and a room's icons: 0 for the vicinity (and inside a host), -DISTRICT_NOISE dice elsewhere. */
62
92
  export function noiseFor(actor, room) {
@@ -299,8 +299,23 @@ function hostInteriorIcons(scene, actor, room) {
299
299
  // and that would put the wrong noun on a person.
300
300
  // Inside a host, the roster is who is INSIDE it (matrix-roster.ts) -- a
301
301
  // persona's body is never in this room.
302
+ // THE HOST'S OWN PROGRAMS ARE LISTED ONCE, BELOW, NOT TWICE, HERE AND
303
+ // BELOW (mPCh2FJykPsyJKmcc's bench, 2026-09-14). A running IC is an
304
+ // actor inside the host AND a name in room.runningIC, and both loops
305
+ // printed it -- in different words, with only the second offering the
306
+ // verbs, so one program read as two and the reader had to guess which
307
+ // line was real:
308
+ //
309
+ // ▲ Patrol IC #2 -- intrusion countermeasures, and you are inside its host.
310
+ // ▲ Patrol IC #2 -- hunting, and it has your icon. (0/10) ("attack ..." crash it)
311
+ //
312
+ // hostInteriorPanel already resolved this the right way round -- it
313
+ // skips `a.icHost === host` in its persona list -- so this matches it
314
+ // rather than inventing a third rule. Everything else inside the host
315
+ // still comes through here: a rival decker's persona, a companion, a
316
+ // construct nothing registered as the host's.
302
317
  for (const a of (room.host ? personasInside(scene, room.host) : [])) {
303
- if (a === actor)
318
+ if (a === actor || a.icHost === room.host)
304
319
  continue;
305
320
  if (a.plane === 'matrix' && sameHostSide(actor, a) && actor.canPerceive(a)) {
306
321
  const companion = scene.ownerGame?.companions.find(c => c.npc === a);
@@ -350,11 +365,16 @@ function hostInteriorIcons(scene, actor, room) {
350
365
  // game will not name, and a room that contradicts the damage log reads
351
366
  // as broken rules rather than thin prose.
352
367
  //
353
- // NO VERB IS OFFERED, deliberately, because none exists yet: IC live
354
- // in room.runningIC as names, not as actors, so nothing in this engine
355
- // can target them -- `thread Resonance Spike` says "no hostile icon in
356
- // reach" while they attack. Naming them here does not pretend
357
- // otherwise; advertising a verb that refuses would be the worse lie.
368
+ // A VERB IS OFFERED NOW, where there is an actor to point it at. When
369
+ // this loop was written IC lived in room.runningIC as names only and
370
+ // nothing could target them, so it deliberately advertised nothing;
371
+ // ic-actors.ts made each running program an actor, and the line grew
372
+ // the monitor and the verbs to match. A kind with no actor behind it
373
+ // (a scene that cannot host actors at all) still gets named and still
374
+ // offers nothing, for the original reason.
375
+ //
376
+ // THIS IS THE ONE PLACE THE HOST'S OWN PROGRAMS ARE LISTED -- see the
377
+ // skip in the persona loop above.
358
378
  for (const name of room.runningIC) {
359
379
  const ice = room.host?.iceActors.get(name);
360
380
  icons.push(`▲ ${ice?.name ?? name} -- hunting, and it has your icon.${ice ? ` (${ice.conditionSummary()}) ("thread resonance-spike ${ice.name}" / "attack ${ice.name}" crash it)` : ''}`);
@@ -1,6 +1,7 @@
1
1
  import { NPC } from '../models/npc.js';
2
2
  import { CommandRegistry } from '../commands/command-registry.js';
3
3
  import { Logger } from './logger.js';
4
+ import { IC_TYPES } from './ic.js';
4
5
  /**
5
6
  * IC AS ICONS (increment 5 of the Matrix position model, 2026-09-02).
6
7
  * SR5 p.247: "Each IC program has a persona with its own Condition
@@ -95,4 +96,51 @@ export function crashedIC(host) {
95
96
  }
96
97
  return out;
97
98
  }
99
+ /**
100
+ * ADOPT A SEEDED CONSTRUCT AS ONE OF ITS HOST'S PROGRAMS
101
+ * (mPCh2FJykPsyJKmcc's bench, 2026-09-14).
102
+ *
103
+ * scene-factory already settles what a matrix-plane NPC IS -- "a
104
+ * matrix-plane NPC in this engine IS intrusion countermeasures" -- and
105
+ * already stands it inside the node it guards. What it never did was
106
+ * tell the HOST. So a seeded construct was ice by every reading except
107
+ * the one the host itself uses, and three things went wrong at once,
108
+ * all of them visible in one bench transcript:
109
+ *
110
+ * 1. THE HOST LAUNCHED ITS TWIN. hostLaunchStep picks the first
111
+ * IC_TYPE not in `runningIC`, and a seeded "Patrol IC" was not in
112
+ * it -- so the host launched a second Patrol on the first Combat
113
+ * Turn and the player fought "Patrol IC" and "Patrol IC #2" at
114
+ * once. Straight against p.247: a host "can't launch more than one
115
+ * of each type of IC program at once."
116
+ * 2. IT ATTACKED WITH NO LIMIT. "IC uses the Matrix attributes of its
117
+ * host" (p.247) is read off `icHost` (Player.baseMatrixAttribute).
118
+ * Unset, a construct's Attack was 0, so every swing it took
119
+ * printed dice.ts's caller-bug diagnostic to the player:
120
+ * `[LIMIT IGNORED -- a caller computed one that is not a limit]`.
121
+ * 3. IT WAS LISTED TWICE, once as an icon inside the host and again
122
+ * off `runningIC` (grid-view.ts), with different words each time.
123
+ *
124
+ * One registration, three symptoms -- which is the argument for fixing
125
+ * it here at the seam rather than three times downstream.
126
+ *
127
+ * THE KIND IS THE CANON NAME WHEN THE SEED USED ONE. A construct called
128
+ * "Patrol IC" IS the host's Patrol, and registering it under its own
129
+ * name would leave the launch step free to add the real one beside it --
130
+ * the bug. A construct with a name of its own ("Sentinel", "the
131
+ * Gatekeeper") registers under that name: it counts against the host's
132
+ * Rating cap because it is a program the host is running, and it blocks
133
+ * no canon type, because it is not one.
134
+ */
135
+ export function adoptSeededIce(npc, host) {
136
+ const canon = IC_TYPES.find(t => t.name.toLowerCase() === npc.name.trim().toLowerCase());
137
+ const kind = canon?.name ?? npc.name;
138
+ npc.icHost = host;
139
+ npc.icKind = kind;
140
+ npc.matrixPosition = { kind: 'host', host };
141
+ if (!host.iceActors.has(kind))
142
+ host.iceActors.set(kind, npc);
143
+ if (!host.runningIC.includes(kind))
144
+ host.runningIC.push(kind);
145
+ }
98
146
  //# sourceMappingURL=ic-actors.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.186.0",
3
+ "version": "5.188.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.",