@maka/maka-cli 5.180.0 → 5.181.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.180.0",
3
+ "version": "5.181.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.",
@@ -267,10 +267,13 @@ take ground. Build it accordingly:
267
267
  Matrix presence, whose files open only to a runner whose BODY is in that room). Pick one
268
268
  and commit to it -- the offline server is the harder, more interesting shape when the
269
269
  fiction wants a vault you must walk into.
270
- - THE OPPOSITION IS ICE, not muscle. Every NPC besides the client is "matrix"-plane and
271
- stationed in the host. No guards patrolling meat corridors, no gunfight in the lobby: a
272
- data run that ends in a firefight is a smash-and-grab wearing the wrong name. A site can
273
- still be WATCHED (cameras) -- surveillance is pressure without being a gun.
270
+ - THE OPPOSITION IS THE HOST'S OWN ICE, not muscle. Give the vault host a "hostRating" of
271
+ 5-8: the host launches its own intrusion countermeasures (Patrol, Probe, Killer...) at
272
+ that rating, so DO NOT write an NPC for the ice -- no "matrix"-plane npcs[] at all. The
273
+ NPCs are the client and the people who work the site. No guards patrolling meat
274
+ corridors, no gunfight in the lobby: a data run that ends in a firefight is a
275
+ smash-and-grab wearing the wrong name. A site can still be WATCHED (cameras) --
276
+ surveillance is pressure without being a gun.
274
277
  - Keep it SHORT and tight. One site, one host, one file.
275
278
  - It pays LESS than a full run of the same tier -- roughly half. The runner is trading pay
276
279
  for a job that never puts a gun in their face, and the fixer prices it that way. Say so
@@ -407,8 +410,9 @@ ${JSON_ONLY}
407
410
  // its description reads.
408
411
  "npcs": [{
409
412
  "name": string, "concept": string, "startLocation": string,
410
- "plane"?: "meat"|"matrix"|"astral", // "matrix" = ICE guarding a hasNode room;
411
- // "astral" = a spirit. Default meat.
413
+ "plane"?: "meat"|"astral", // "astral" = a spirit. Default meat. NEVER "matrix": a host's
414
+ // ice is not a person -- the host launches its own programs
415
+ // at its "hostRating", and an NPC written as ice is dropped.
412
416
  "hostile"?: boolean // OPPOSITION: this one KNOWS the runner does not belong and will
413
417
  // act on it -- a posted guard, a patrolling enforcer, a gang
414
418
  // holding the room. Default false, and LEAVE IT FALSE unless the
@@ -418,9 +422,6 @@ ${JSON_ONLY}
418
422
  // building, and marking them turns an ordinary scene into a
419
423
  // brawl. If in doubt, leave it out -- a guard who stays calm is
420
424
  // a missed beat, a hostile bartender is a broken scene.
421
- // ICE NEEDS NO FLAG: a "matrix" NPC is intrusion countermeasures
422
- // and is treated as opposition automatically. Write it only to
423
- // say FALSE, for the rare tame construct.
424
425
  }],
425
426
  "devices": [{
426
427
  "name": string, "concept": string, // what it physically IS (a maglock, a keypad, a
@@ -520,9 +521,10 @@ Hard structural rules (validated mechanically -- a violation is rejected):
520
521
  skill can still take the shortcut -- pick the lock, hack the panel -- but the found way
521
522
  must exist for a runner who has neither.
522
523
  - The winCondition item must not be heldBy winCondition.toNpc. If a matrix-plane item is the
523
- winCondition item, its room has hasNode true AND a "matrix"-plane NPC stationed there. If
524
- the runner can jack in, a Datachip/Document winCondition item MUST be that matrix-plane
525
- item -- data for a hacker lives on a host, never on a desk.
524
+ winCondition item, its room has hasNode true AND a "hostRating" of at least 4 -- the host
525
+ defends its own files with its own ice; never write an NPC for that. If the runner can
526
+ jack in, a Datachip/Document winCondition item MUST be that matrix-plane item -- data for
527
+ a hacker lives on a host, never on a desk.
526
528
  - ${isContinuation
527
529
  ? 'ZERO "starting"-role items -- the runner arrives with everything they own. 4-6'
528
530
  : '3-5 "starting"-role items (basics the runner doesn\'t already own -- see the dossier), 4-6'}
@@ -652,8 +654,8 @@ For EVERY npc in the skeleton, expand its "concept":
652
654
  "hacking"?, "cybercombat"?, "electronic-warfare"?, "hardware"?, "locksmith"?, "computer"?,
653
655
  "negotiation"?, "con"?, "intimidation"?, "etiquette"?, "leadership"?, "performance"?, "disguise"?,
654
656
  "medicine"?, "assensing"?, "counterspelling"?, "conjuring"?, "gunnery"? } }
655
- "magic" 3-6 ONLY for a genuinely Awakened concept; "plane":"matrix" ice fights with
656
- logic 4-7; give combat blocks to anyone the story expects in a firefight. "skills" is
657
+ "magic" 3-6 ONLY for a genuinely Awakened concept; give combat blocks to anyone the story
658
+ expects in a firefight. "skills" is
657
659
  optional flavor for defined concepts (a sharpshooter's "firearms": 6).
658
660
 
659
661
  ${JSON_ONLY}
@@ -886,6 +888,14 @@ const GENERIC_LOCK_WORDS = new Set([
886
888
  'lock', 'maglock', 'door', 'gate', 'grate', 'hatch', 'safe', 'vault', 'cage', 'shutter', 'padlock', 'cabinet', 'locker',
887
889
  'keycard', 'card', 'passkey', 'access', 'security', 'back', 'front', 'side', 'room', 'office', 'main', 'inner', 'outer',
888
890
  ]);
891
+ /**
892
+ * THE LEAST A VAULT HOST RATES. A host launches its own IC at its rating
893
+ * (p.247, one per Combat Turn up to the rating), so the rating is the
894
+ * whole defence of the paydata inside it; below this the run is a free
895
+ * walk. Four is the floor: a rating-3 default host fields three thin
896
+ * programs and no Killer.
897
+ */
898
+ export const VAULT_HOST_RATING = 4;
889
899
  export function validateSkeleton(skeleton, player, crew) {
890
900
  const fail = (msg) => { throw new Error(`Skeleton invalid: ${msg}`); };
891
901
  if (!Array.isArray(skeleton?.rooms) || skeleton.rooms.length === 0)
@@ -1107,11 +1117,18 @@ export function validateSkeleton(skeleton, player, crew) {
1107
1117
  // Worded to route to 'skeleton' (no "items[", no "Win condition item").
1108
1118
  const winItem = skeleton.items.find(i => i.name === skeleton.winCondition.item);
1109
1119
  if (winItem && isDataShaped(winItem.category) && winItem.plane !== 'matrix' && partyCanDeck(player)) {
1110
- fail(`the winCondition item "${winItem.name}" is a ${winItem.category} on the meat plane, and this runner can jack in -- for a decker or technomancer, data is PAYDATA: give it "plane": "matrix", place it in a hasNode room (not heldBy anyone), and station a "matrix"-plane NPC (ice) there. A chip lying on a desk is no job for a hacker.`);
1120
+ fail(`the winCondition item "${winItem.name}" is a ${winItem.category} on the meat plane, and this runner can jack in -- for a decker or technomancer, data is PAYDATA: give it "plane": "matrix", place it in a hasNode room (not heldBy anyone) with a hostRating of ${VAULT_HOST_RATING} or more. A chip lying on a desk is no job for a hacker.`);
1111
1121
  }
1112
1122
  // The other half of the same rule, for ANY matrix-plane objective: the
1113
- // schema says its room has hasNode and an ice NPC stationed there, and
1123
+ // schema says its room has hasNode and a rating that fields ice, and
1114
1124
  // until now nothing at the skeleton checked it.
1125
+ //
1126
+ // THE HOST IS ITS OWN GUARD (player ruling 2026-09-13, "mute all
1127
+ // ice"): a vault used to demand a "matrix"-plane NPC -- a person
1128
+ // wearing an ice costume, with dialogue lines and a model behind it,
1129
+ // which is how four programs ended up bargaining over a chip in Vex's
1130
+ // log. Canon has none of that: the host launches its own IC at its
1131
+ // rating (p.247), so what a vault needs is a rating worth the name.
1115
1132
  if (winItem?.plane === 'matrix') {
1116
1133
  if (!winItem.room) {
1117
1134
  fail(`the winCondition item "${winItem.name}" is matrix-plane paydata but has no "room" -- data lives on a host, never in a pocket. Place it in a hasNode room.`);
@@ -1120,9 +1137,17 @@ export function validateSkeleton(skeleton, player, crew) {
1120
1137
  if (!vault?.hasNode) {
1121
1138
  fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in "${winItem.room}", which has no "hasNode": true -- a matrix file can only exist on a host. Mark that room hasNode.`);
1122
1139
  }
1123
- const ice = skeleton.npcs.some(n => n.plane === 'matrix' && n.startLocation === winItem.room);
1124
- if (!ice) {
1125
- fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in hasNode room "${winItem.room}" with no ice guarding it -- station a "matrix"-plane NPC there. An undefended vault is a free win.`);
1140
+ else if ((vault.hostRating ?? 0) < VAULT_HOST_RATING) {
1141
+ fail(`the winCondition item "${winItem.name}" is matrix-plane paydata in hasNode room "${winItem.room}" whose hostRating is ${vault.hostRating ?? 'unset'} -- a vault defends itself with its own ice, so give that room a hostRating of ${VAULT_HOST_RATING} or more. An undefended vault is a free win.`);
1142
+ }
1143
+ }
1144
+ // NO NPC IS ICE. The same ruling from the other side: a model that
1145
+ // still writes a "matrix"-plane person is writing a voice for a
1146
+ // program. normalizeSkeleton drops them with a note; this holds the
1147
+ // line for anything that reaches the validator by another road.
1148
+ for (const npc of skeleton.npcs) {
1149
+ if (npc.plane === 'matrix') {
1150
+ fail(`npc "${npc.name}" is "matrix"-plane -- ice is not a person. Remove it; the host over its room fields its own countermeasures at its hostRating.`);
1126
1151
  }
1127
1152
  }
1128
1153
  // EVERY HOST HAS A PURPOSE AND HOLDS SOMETHING (N4erz3f63MiCZLkDE,
@@ -1153,10 +1178,9 @@ export function validateSkeleton(skeleton, player, crew) {
1153
1178
  if (!holds) {
1154
1179
  fail(`room "${room.name}" is a host (${room.hostPurpose}) with NOTHING inside it -- no "matrix"-plane item placed in that room. A host holds what its business protects: put at least one matrix-plane item there (the ledger, the client list, the camera archive), or drop hasNode.`);
1155
1180
  }
1156
- const guarded = skeleton.npcs.some(n => n.plane === 'matrix' && n.startLocation === room.name);
1157
- if (!guarded) {
1158
- fail(`room "${room.name}" is a host holding matrix-plane files with no ice on it -- station a "matrix"-plane NPC in that room. A host worth entering is a host that defends itself.`);
1159
- }
1181
+ // "Ice stationed on it" used to be the third requirement. It is
1182
+ // now the host's own rating (see the vault rule above): a host
1183
+ // defends itself, and a person written as its ice is refused.
1160
1184
  }
1161
1185
  // Lock-and-key reachability. CRITICAL: edges are BIDIRECTIONAL -- the
1162
1186
  // engine mirrors every declared exit into a shared two-way Door
@@ -1568,6 +1592,28 @@ export function normalizeSkeleton(skeleton) {
1568
1592
  delete room.nodeAccess;
1569
1593
  notes.push(`dropped the host over "${room.name}" -- it held no matrix-plane file, and a host with nothing in it should not be there.`);
1570
1594
  }
1595
+ // ICE IS NOT A PERSON (player ruling 2026-09-13, "mute all ice"). The
1596
+ // prompt no longer asks for a "matrix"-plane NPC and validateSkeleton
1597
+ // refuses one; a model that writes one anyway is writing a voice for
1598
+ // a program, and the cheap repair is to drop it and let the host's
1599
+ // own rating do the guarding. The vault host gets that rating here
1600
+ // when the model forgot it -- clamp, don't burn a regeneration.
1601
+ const constructs = (skeleton.npcs ?? []).filter(n => n.plane === 'matrix');
1602
+ if (constructs.length > 0) {
1603
+ skeleton.npcs = skeleton.npcs.filter(n => n.plane !== 'matrix');
1604
+ for (const n of constructs) {
1605
+ notes.push(`normalized: dropped npc "${n.name}" -- it was "matrix"-plane ice, and a host fields its own countermeasures at its hostRating rather than a person in a costume.`);
1606
+ }
1607
+ }
1608
+ const winItem = (skeleton.items ?? []).find(i => i.name === skeleton.winCondition?.item);
1609
+ if (winItem?.plane === 'matrix' && winItem.room) {
1610
+ const vault = (skeleton.rooms ?? []).find(r => r.name === winItem.room);
1611
+ if (vault?.hasNode && (vault.hostRating ?? 0) < VAULT_HOST_RATING) {
1612
+ const was = vault.hostRating === undefined ? 'no hostRating' : `hostRating ${vault.hostRating}`;
1613
+ vault.hostRating = VAULT_HOST_RATING;
1614
+ notes.push(`normalized: the vault host over "${vault.name}" had ${was}; raised it to ${VAULT_HOST_RATING} so it fields ice worth the paydata.`);
1615
+ }
1616
+ }
1571
1617
  // NOBODY MINDING THE STORE (playtest 2026-08-25: "the flooded
1572
1618
  // undermarket could use some details -- it's just blank"). It was the
1573
1619
  // district's only commerce room with neither a vendor nor stock: a
@@ -8,7 +8,7 @@ import { clampDeviceKind } from '../utilities/affordances.js';
8
8
  import { GenerationCapture } from '../utilities/generation-capture.js';
9
9
  import { fetchCanonContext } from '../utilities/canon-lore.js';
10
10
  import { SceneSynthesizer } from './scene-factory.js';
11
- import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, } from './scene-chunks.js';
11
+ import { buildSkeletonPrompt, buildRoomsPrompt, buildNpcsPrompt, buildDevicesPrompt, buildItemsPrompt, pickRunType, parseJsonReply, validateSkeleton, normalizeSkeleton, assembleScene, classifyRepair, VAULT_HOST_RATING, } from './scene-chunks.js';
12
12
  // The fixer name convention and the player dossier live with the prompt
13
13
  // builders now (see scene-chunks.ts); re-exported so the existing
14
14
  // import sites (call.ts, game.ts) keep working unchanged.
@@ -751,9 +751,11 @@ export class SceneSeedGenerator {
751
751
  if (!room?.hasNode) {
752
752
  throw new Error(`Matrix-plane win condition item "${wc.item}" is in room "${item.room}", which has no "hasNode": true -- matrix files can only exist on a host. Mark that room hasNode.`);
753
753
  }
754
- const iceGuard = (candidate.npcs ?? []).some(n => n.plane === 'matrix' && n.player.startLocation === item.room);
755
- if (!iceGuard) {
756
- throw new Error(`Matrix-plane win condition item "${wc.item}" sits in hasNode room "${item.room}" with NO ice guarding it -- add an npc with "plane": "matrix", "startLocation": "${item.room}", and a combat block (logic 4-7). An undefended data vault makes the run a free win.`);
754
+ // THE HOST GUARDS ITSELF (2026-09-13): this used to demand an ice
755
+ // NPC; a host's countermeasures are its own programs at its rating
756
+ // (p.247), and normalizeSkeleton raises a vault to the floor.
757
+ if ((room.hostRating ?? 0) < VAULT_HOST_RATING) {
758
+ throw new Error(`Matrix-plane win condition item "${wc.item}" sits in hasNode room "${item.room}" whose hostRating is ${room.hostRating ?? 'unset'} -- a vault defends itself with its own ice; give that room a hostRating of ${VAULT_HOST_RATING} or more. An undefended data vault makes the run a free win.`);
757
759
  }
758
760
  }
759
761
  /**
@@ -935,6 +935,29 @@ description text.`;
935
935
  combatPhaseLocked() {
936
936
  return !!this._scene?.encounterFor?.(this);
937
937
  }
938
+ /**
939
+ * ICE HAS NO VOICE AND NO MIND OF ITS OWN (player ruling 2026-09-13,
940
+ * from Vex's Borrowed Time log: "Patrol IC: That chip belongs to
941
+ * Corvid. Put it back on the desk, nice and slow" -- four programs
942
+ * bargaining over a chip in four voices, and Killer IC "assembling
943
+ * in the back room" after the persona had already jacked out).
944
+ *
945
+ * SR5 p.247: IC is "mercilessly efficient, not very bright" -- it
946
+ * finds, disables, destroys and repels, and nothing in the books has
947
+ * it speak. A spawned program (icKind) and a seeded matrix-plane
948
+ * construct are the same thing to the engine: driven by dice
949
+ * (utilities/ic.ts), never by the model. No speech, no reflection,
950
+ * no exposition, and no listening either -- there is nothing for a
951
+ * stimulus to wake.
952
+ */
953
+ isConstruct() {
954
+ return this.icKind !== undefined || this.plane === 'matrix';
955
+ }
956
+ /** Every AI entry point asks this: a construct never runs the model,
957
+ * and anyone in a Combat Turn yields to the turn loop. */
958
+ aiSuspended() {
959
+ return this.isConstruct() || this.combatPhaseLocked();
960
+ }
938
961
  /**
939
962
  * One verb, executed now, no reflection -- the brain's way of acting
940
963
  * inside an Action Phase. Returns the command's own result so the
@@ -963,6 +986,10 @@ description text.`;
963
986
  this.logger.write(`${this.name} is gone from the world -- dropping queued action "${commandString}".`);
964
987
  return;
965
988
  }
989
+ if (this.isConstruct()) {
990
+ this.logger.write(`${this.name} is ice -- the host's dice act for it; dropping "${commandString}".`);
991
+ return;
992
+ }
966
993
  if (this.combatPhaseLocked()) {
967
994
  this.logger.write(`${this.name} is in a Combat Turn -- the turn loop acts for them; dropping "${commandString}".`);
968
995
  return;
@@ -995,6 +1022,10 @@ description text.`;
995
1022
  // removed shell's AI.
996
1023
  if (this.deathClaimed)
997
1024
  return;
1025
+ // NOR DOES ICE (isConstruct): a program has no conversation to
1026
+ // hold and no history worth keeping.
1027
+ if (this.isConstruct())
1028
+ return;
998
1029
  // NEITHER DO THE UNCONSCIOUS. A knocked-out actor now STAYS in the
999
1030
  // scene so they can be brought round (player ruling 2026-08-25) --
1000
1031
  // which means every AI entry point has to check, or the body on the
@@ -1113,6 +1144,10 @@ description text.`;
1113
1144
  // Same cross-plane blindness as hear() above.
1114
1145
  if (!this.canPerceive(actor))
1115
1146
  return;
1147
+ // Ice watches with the host's dice, not with a model (see
1148
+ // isConstruct): what it notices, utilities/ic.ts rolls for.
1149
+ if (this.isConstruct())
1150
+ return;
1116
1151
  // The unconscious see nothing (see hear()): a KO'd actor stays in
1117
1152
  // the scene now, and must not react to what happens over it.
1118
1153
  if (this.isIncapacitated())
@@ -1428,8 +1463,8 @@ description text.`;
1428
1463
  this.logger.write(`${this.name} is gone from the world -- reflect chain ends.`);
1429
1464
  return;
1430
1465
  }
1431
- if (this.combatPhaseLocked()) {
1432
- this.logger.write(`${this.name} is in a Combat Turn -- reflect chain yields to the turn loop.`);
1466
+ if (this.aiSuspended()) {
1467
+ this.logger.write(`${this.name} ${this.isConstruct() ? 'is ice -- no reflect chain' : 'is in a Combat Turn -- reflect chain yields to the turn loop'}.`);
1433
1468
  return;
1434
1469
  }
1435
1470
  // THE GRIND BREAKER. Record what just happened, then refuse to keep
@@ -1609,8 +1644,8 @@ ${worldEventsSummary}
1609
1644
  : [];
1610
1645
  }
1611
1646
  async respondTo(actor, message) {
1612
- if (this.combatPhaseLocked()) {
1613
- this.logger.write(`${this.name} is in a Combat Turn -- no AI reaction to "${message.slice(0, 40)}".`);
1647
+ if (this.aiSuspended()) {
1648
+ this.logger.write(`${this.name} ${this.isConstruct() ? 'is ice' : 'is in a Combat Turn'} -- no AI reaction to "${message.slice(0, 40)}".`);
1614
1649
  return;
1615
1650
  }
1616
1651
  this.logger.write('Attempting to respond');
@@ -59,6 +59,9 @@ export function alarmedStaff(scene, player) {
59
59
  .filter((a) => a instanceof NPC)
60
60
  .filter(npc => !isEphemeral(npc))
61
61
  .filter(npc => npc.allyOf === undefined)
62
+ // Ice is not staff: a program answers to the host's dice
63
+ // (utilities/ic.ts), never to an alarm beat (NPC.isConstruct).
64
+ .filter(npc => !npc.isConstruct())
62
65
  .filter(npc => !npc.isIncapacitated());
63
66
  }
64
67
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.180.0",
3
+ "version": "5.181.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.",