@maka/maka-cli 5.212.0 → 5.214.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.212.0",
3
+ "version": "5.214.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.",
@@ -150,7 +150,7 @@ export class BacklogCommand extends Command {
150
150
  // patch-probe exists to catch. game.ts already imports this module
151
151
  // lazily for the same reason (streetTransport).
152
152
  const { cliVersion, engineVersion } = await import('../utilities/shared-run.js');
153
- const outcome = await submitBacklog(text, {
153
+ const filed = await submitBacklog(text, {
154
154
  runnerName: this.actor?.name,
155
155
  // Links the item back to the play session it came out of -- the
156
156
  // same id the uploaded logs are keyed by, so an item and its
@@ -177,10 +177,21 @@ export class BacklogCommand extends Command {
177
177
  client: currentSession()?.client ?? 'cli',
178
178
  });
179
179
  // A real call classifies the account for free -- no second request.
180
- noteBacklogOutcome(outcome);
181
- if (outcome !== 'ok')
182
- return this.explain(outcome);
183
- return `{cyan-fg}Filed. It'll come back checked against the books.${hint(` ("backlog list" reads the board.)`)}{/cyan-fg}`;
180
+ noteBacklogOutcome(filed.outcome);
181
+ if (filed.outcome !== 'ok')
182
+ return this.explain(filed.outcome);
183
+ // NAME THE THING YOU JUST FILED (X3tZu4rspsa5CQW38, Maka: 'Let's
184
+ // change this to read "Logged - <ID>"'). "Filed. It'll come back
185
+ // checked against the books." told the reporter a report exists
186
+ // somewhere and gave them no way to refer to it -- and the id was on
187
+ // the wire the whole time, thrown away by submitBacklog.
188
+ //
189
+ // The old sentence is kept for the case where the server answered
190
+ // 200 with a body we could not read: the item IS filed, and saying
191
+ // "Logged - undefined" would be worse than saying less.
192
+ return filed.id
193
+ ? `{cyan-fg}Logged - ${filed.id}${hint(` ("backlog list" reads the board.)`)}{/cyan-fg}`
194
+ : `{cyan-fg}Filed. It'll come back checked against the books.${hint(` ("backlog list" reads the board.)`)}{/cyan-fg}`;
184
195
  }
185
196
  /**
186
197
  * THE REVIEW QUEUE (user request 2026-08-27). Bare "backlog review"
@@ -89,6 +89,15 @@ export class BypassCommand extends Command {
89
89
  }
90
90
  return { error: `There's nothing here called "${raw}".` };
91
91
  }
92
+ /**
93
+ * THE ACTION PHASE COST OF ONE ATTEMPT, charged at the moment the
94
+ * attempt becomes real (see execute()). Free by default; a subclass
95
+ * whose verb IS a canon Matrix or combat action overrides it and
96
+ * returns the refusal when the phase cannot pay.
97
+ */
98
+ billForAttempt() {
99
+ return undefined;
100
+ }
92
101
  async execute(args) {
93
102
  const room = this.actor.currentLocation;
94
103
  const all = room?.openableDevices() ?? [];
@@ -164,6 +173,19 @@ export class BypassCommand extends Command {
164
173
  if (reach.refusal)
165
174
  return reach.refusal;
166
175
  const crossNote = reach.line ? `${reach.line}\n` : '';
176
+ // THE LAST POINT BEFORE THE ATTEMPT IS REAL -- the target resolved,
177
+ // the verb allowed, the runner standing where the mechanism is. A
178
+ // subclass with an Action Phase cost pays it HERE and nowhere
179
+ // earlier, so every refusal above is free (GoEyGG9PSRM4mpXzG: "I
180
+ // shouldn't be hit on a simple/complex action if the command is
181
+ // rejected").
182
+ //
183
+ // Default is free: most verbs on this base (pick, breach, use) have
184
+ // never billed an action and this is not the change that gives them
185
+ // one. Only HackCommand overrides it today.
186
+ const billed = this.billForAttempt();
187
+ if (billed)
188
+ return billed;
167
189
  const outcome = await this.determineOutcome(device);
168
190
  if (outcome === 'progress') {
169
191
  this.logger.write(`${this.actor.name} keeps working "${device.name}" (${this.verbName}, in progress)`);
@@ -13,7 +13,7 @@ import { TapCommand } from './tap.js';
13
13
  import { nameableNames } from '../utilities/nameables.js';
14
14
  import { sameHostSide } from '../models/player.js';
15
15
  import { MAX_MARKS } from '../utilities/marks.js';
16
- import { billAction } from '../utilities/action-cost.js';
16
+ import { actionRefusal, billAction } from '../utilities/action-cost.js';
17
17
  import { showsMechanics, mechanicsActorPrefix } from '../utilities/mechanics-audience.js';
18
18
  import { declarationPenalty, parseMarkDeclaration, hostDefensePool, wanDefensePool, freeMatrixPerceptionHits, bruteForceMatrixDv, overwatchFromDefense, GO_BIG_QUALITY, deviceDefensePool } from '../utilities/matrix-intrusion.js';
19
19
  /**
@@ -54,6 +54,23 @@ export class HackCommand extends BypassCommand {
54
54
  actionName() {
55
55
  return this.mode() === 'attack' ? 'Brute Force' : 'Hack on the Fly';
56
56
  }
57
+ /**
58
+ * THE SPEND, at the moment the verb commits -- see the long note in
59
+ * execute() for why it is not at the top any more
60
+ * (GoEyGG9PSRM4mpXzG). Called immediately before a roll, or before
61
+ * handing off to a command that does the work. Returns the refusal
62
+ * when the phase cannot pay, undefined when it just did.
63
+ */
64
+ bill() {
65
+ return billAction(this.scene, this.actor, 'complex', this.actionName());
66
+ }
67
+ /** The meat-world barrier route (super.execute) pays at the same
68
+ * moment every other route does -- once the device is resolved and
69
+ * the attempt is about to happen, never for a name that matched
70
+ * nothing. */
71
+ billForAttempt() {
72
+ return this.bill();
73
+ }
57
74
  async execute(args = []) {
58
75
  const mode = this.mode();
59
76
  const afterMode = [...(args ?? [])];
@@ -66,9 +83,26 @@ export class HackCommand extends BypassCommand {
66
83
  // BRUTE FORCE and HACK ON THE FLY are Complex Actions (p.238, p.240)
67
84
  // -- billed only inside a Combat Turn, where one intrusion is the
68
85
  // whole Action Phase.
69
- const bill = billAction(this.scene, this.actor, 'complex', this.actionName());
70
- if (bill)
71
- return bill;
86
+ //
87
+ // ASKED HERE, PAID LATER (GoEyGG9PSRM4mpXzG, Maka: "I shouldn't be
88
+ // hit on a simple/complex action if the command is rejected").
89
+ // This used to be the spend, at the top, before a single line of
90
+ // target resolution had run -- so "hack camera" in a room with no
91
+ // camera printed "Nothing on the grid answers to 'camera'" AND ate
92
+ // the whole Action Phase. The player was charged an intrusion for a
93
+ // word the parser did not recognise.
94
+ //
95
+ // The affordability question still belongs up here: a runner with
96
+ // nothing left this phase should hear it before the game goes
97
+ // hunting through PANs, devices and hosts on their behalf. What
98
+ // moved is the SPEND -- down to `bill()` below, called at each point
99
+ // the verb actually commits: immediately before a roll, or before
100
+ // handing off to a command that does the work (TapCommand, the
101
+ // barrier bypass in super.execute). Every refusal between here and
102
+ // there costs nothing, which is the report.
103
+ const unaffordable = actionRefusal(this.scene, this.actor, 'complex');
104
+ if (unaffordable)
105
+ return unaffordable;
72
106
  // PAN warfare: "hack <name>" cracks a meat actor's personal area
73
107
  // network -- from inside the Matrix, or right here in AR with a
74
108
  // working deck in hand (which is also how enemy deckers get YOU).
@@ -99,6 +133,11 @@ export class HackCommand extends BypassCommand {
99
133
  // puzzle that happens to be named for one still falls through to
100
134
  // the bypass path below, untouched.
101
135
  if (rest.length > 0 && matchesCameraName(rest.join(' ')) && isWatched(this.actor.currentLocation)) {
136
+ // NOT BILLED HERE. TapCommand has its own refusals past this point
137
+ // -- a direct connection is a cable and wants your BODY in the
138
+ // room -- so it pays at its own commit point instead. Billing
139
+ // before the handoff charged a Complex Action for being told the
140
+ // route was not available (GoEyGG9PSRM4mpXzG, one layer down).
102
141
  return new TapCommand({
103
142
  actor: this.actor,
104
143
  scene: this.scene,
@@ -208,6 +247,12 @@ export class HackCommand extends BypassCommand {
208
247
  const burned = this.actor.burnedAttributeRefusal(mode === 'attack' ? 'attack' : 'sleaze');
209
248
  if (burned)
210
249
  return burned;
250
+ // Committed: a real host, resolved by name, with marks left to
251
+ // take and a bracket to take them with. Every refusal above this
252
+ // line is free.
253
+ const billedHost = this.bill();
254
+ if (billedHost)
255
+ return billedHost;
211
256
  this.actor.performAction('hacks the host', room.name, { quiet: this.scene.isHumanControlled(this.actor) });
212
257
  const attempt = await this.hostIntrusionRoll(mode, declared, room);
213
258
  const success = attempt.success;
@@ -592,6 +637,13 @@ export class HackCommand extends BypassCommand {
592
637
  const burned = actor.burnedAttributeRefusal(mode === 'attack' ? 'attack' : 'sleaze');
593
638
  if (burned)
594
639
  return burned;
640
+ // Committed: a real slaved icon, named and reachable. Note that
641
+ // every `return null` above is a FALL-THROUGH, not a refusal -- it
642
+ // hands the query on to the host or PAN route, which pays for
643
+ // itself if it commits. Billing before those would charge twice.
644
+ const billedDevice = this.bill();
645
+ if (billedDevice)
646
+ return billedDevice;
595
647
  const declared = Math.min(declaredMarks, MAX_MARKS - held);
596
648
  const declarePenalty = declarationPenalty(declared, actor.hasQuality(GO_BIG_QUALITY));
597
649
  const silence = actor.matrixActionPenalty;
@@ -845,6 +897,10 @@ export class HackCommand extends BypassCommand {
845
897
  const burnedPan = actor.burnedAttributeRefusal(mode === 'attack' ? 'attack' : 'sleaze');
846
898
  if (burnedPan)
847
899
  return burnedPan;
900
+ // Committed: a real person, in reach, running a PAN worth cracking.
901
+ const billedPan = this.bill();
902
+ if (billedPan)
903
+ return billedPan;
848
904
  const limit = actor.matrixAttribute(mode === 'attack' ? 'attack' : 'sleaze');
849
905
  // Logic + the better of HACKING and ELECTRONIC WARFARE + gear (canon:
850
906
  // EW is the jamming-and-signals half of PAN warfare; best-of keeps
@@ -1,4 +1,5 @@
1
1
  import { Command } from './command.js';
2
+ import { billAction } from '../utilities/action-cost.js';
2
3
  import { hostLabel } from '../utilities/grid-names.js';
3
4
  import { rollPool, formatRoll } from '../utilities/dice.js';
4
5
  import { accrueOverwatch } from '../utilities/overwatch.js';
@@ -113,6 +114,21 @@ export class TapCommand extends Command {
113
114
  return burned;
114
115
  const limit = actor.matrixAttribute(mode === 'attack' ? 'attack' : 'sleaze');
115
116
  const pool = Math.max(1, actor.logic + actor.skillRating('hacking') + actor.bonus('hacking') + silence + actor.woundModifier - actor.sustainingPenalty);
117
+ // THE SPEND, HERE AND NOT EARLIER (GoEyGG9PSRM4mpXzG). A direct
118
+ // connection does not change WHICH action you are taking -- it is
119
+ // still Hack on the Fly or Brute Force (p.240/p.238), and both are
120
+ // Complex Actions -- but every refusal above this line is a reason
121
+ // the attempt never happened: no cameras, no cable, no host on the
122
+ // other end, the host already open, the ring already full, a bracket
123
+ // eaten to zero. None of those is an action taken.
124
+ //
125
+ // It matters twice, because hack.ts routes "hack camera" here. That
126
+ // route USED to bill before delegating, which meant a decker out on
127
+ // the grid typing "hack camera" paid a Complex Action to be told a
128
+ // direct connection is a CABLE and needs their body in the room.
129
+ const billed = billAction(this.scene, actor, 'complex', mode === 'attack' ? 'Brute Force' : 'Hack on the Fly');
130
+ if (billed)
131
+ return billed;
116
132
  actor.performAction('cables into the cameras', `${room.name} -- a hand on the housing, a filament out of their deck`);
117
133
  const roll = rollPool(pool, limit);
118
134
  if (showsMechanics(this.scene, actor)) {
@@ -1009,5 +1009,82 @@
1009
1009
  // friction. The refusal returns before the item moves, so a runner out
1010
1010
  // of actions keeps both the file and the turn, and listing what is in
1011
1011
  // reach ("download" bare) still costs nothing.
1012
- export const ENGINE_VERSION = '1.72.0';
1012
+ // 1.73.0 (2026-09-16): A REJECTED COMMAND IS FREE (GoEyGG9PSRM4mpXzG,
1013
+ // Maka: "I shouldn't be hit on a simple/complex action if the command
1014
+ // is rejected: 'Nothing on the grid answers to \"camera\" -- no host by
1015
+ // that name, no PAN, no camera ...' should not count as a complex
1016
+ // action"). hack.ts billed its Complex Action at the TOP of execute(),
1017
+ // before one line of target resolution had run, so a name the grid
1018
+ // does not answer to cost exactly what an intrusion costs: the player
1019
+ // was told nothing matched and their Action Phase was gone. Canon
1020
+ // charges for an action TAKEN (p.163-167); a sentence the parser
1021
+ // refused is not one.
1022
+ // - The AFFORDABILITY question stays at the top, where it belongs: a
1023
+ // runner with nothing left this phase should hear that before the
1024
+ // game goes hunting through PANs, devices and hosts on their behalf.
1025
+ // That is the new actionRefusal() in utilities/action-cost.ts --
1026
+ // billAction's question without billAction's spend, same budget,
1027
+ // same wording, so the player cannot tell which one spoke.
1028
+ // - The SPEND moved down to each route's commit point: immediately
1029
+ // before the host intrusion roll, the slaved-device roll and the PAN
1030
+ // roll, and (for the meat-world barrier route) at the new
1031
+ // billForAttempt() hook in bypass-command.ts, which fires once the
1032
+ // device is resolved, the verb allowed and the runner standing where
1033
+ // the mechanism is. The hook is free by default -- pick, breach and
1034
+ // use have never billed an action and this is not the change that
1035
+ // gives them one.
1036
+ // - THE SAME DEFECT WAS ONE LAYER DOWN, found by the test rather than
1037
+ // by reading: "hack camera" RESOLVES in a watched room and hands off
1038
+ // to TapCommand, which then refuses because a direct connection is a
1039
+ // cable and wants your BODY in the room. Billing before the handoff
1040
+ // charged a Complex Action for that refusal. tap.ts now pays at its
1041
+ // own commit point, which also gives the `tap` verb the cost it
1042
+ // always owed -- a direct connection does not change WHICH action
1043
+ // you are taking (p.233), and both halves are Complex (p.238/p.240).
1044
+ // Note the shape, because it is easy to write again: a verb that bills
1045
+ // at the top of execute() has charged for the phase before it knows
1046
+ // whether it has a target, and a verb that bills before delegating has
1047
+ // charged for a refusal it cannot see.
1048
+ // 1.74.0 (2026-09-16): A BEAT IS WHAT THE ROOM CAN SEE AND HEAR
1049
+ // (BDYYpN9EZJtTJ5B6A, Maka: 'I shouldn't "notice" skitter's inner
1050
+ // monolog: "Skitter's eyes narrow--sharp as a razor. The kid just
1051
+ // walked off with product without settling up, and now he's shouting
1052
+ // about it like it's some kinda joke."'). The first sentence is what
1053
+ // a beat is FOR. The second is Skitter's private assessment, printed
1054
+ // to a player with no way to know it -- and a player who reads an
1055
+ // NPC's judgement of them cannot un-know it.
1056
+ // - The brief said only "Any exposition should be in 3rd person",
1057
+ // which constrains grammatical person and says nothing about
1058
+ // observability -- it licensed exactly this. A new bullet draws the
1059
+ // line where it belongs: exposition is what someone standing in the
1060
+ // room could see or hear, never what you know, have worked out,
1061
+ // think of them, or are about to do.
1062
+ // - An INTERIOR rule joins CONTACT, COMPELLED_MOVEMENT and
1063
+ // SELF_LOCATION in narration-limits.ts, because an instruction is
1064
+ // advice and this file is the engine. It catches interiority that
1065
+ // ANNOUNCES itself: a mind named as a place ("in his head", "in her
1066
+ // mind"), a certainty no watcher could have ("knows exactly", "knew
1067
+ // full well"), and silent calculation as an event ("running the
1068
+ // math").
1069
+ // - MEASURED OVER ALL 294 exposition beats the site has stored, not a
1070
+ // sample: five match, all five are the bug, and the newest is from
1071
+ // TODAY ("The file updates itself quietly in his head") -- so this
1072
+ // is live, not historical. Nothing else in the corpus matches.
1073
+ // - THE NARROWNESS IS THE DESIGN. Good beats describe inner life
1074
+ // constantly, from the OUTSIDE: "something behind her eyes does the
1075
+ // arithmetic", "the math of a fixer deciding whether she's being
1076
+ // played", "in a way that says she's heard this sound too many
1077
+ // times". A pattern on "math", "deciding" or a bare "knows" eats
1078
+ // every one of them, and a gate that eats those is worse than the
1079
+ // defect. Those six lines are pinned as must-not-eat tests.
1080
+ // - AND THE HONEST LIMIT, pinned as a test of its own: the reported
1081
+ // sentence carries NO marker. It is free indirect discourse, a
1082
+ // thought with the "he knows" filed off, and no regex can tell it
1083
+ // from a statement of fact. The brief is what aims at that shape;
1084
+ // the rule does not pretend to.
1085
+ // (The author-voice leaks in the same corpus -- "I'll have Deacon Ruy
1086
+ // stay guarded...", "Suspicious system-note injection detected" --
1087
+ // are all dated 2026-08-24 and predate isOutOfCharacter, which
1088
+ // already catches them. Checked, not assumed.)
1089
+ export const ENGINE_VERSION = '1.74.0';
1013
1090
  //# sourceMappingURL=engine-version.js.map
@@ -2353,6 +2353,7 @@ You are an NPC named ${this.name} in a gritty cyberpunk text adventure game.
2353
2353
 
2354
2354
  - Stay in character. Respond naturally based on your surroundings, personality, and the player’s message.
2355
2355
  - Any exposition should be in 3rd person. Keep exposition to bare minimum.
2356
+ - Exposition is ONLY what someone standing in the room could see or hear. Never your own thoughts, judgements, conclusions, or memories -- not what you know, not what you have worked out, not what you think of them, not what you are about to do. Write the OUTSIDE of it: the look, the pause, the hand that stops moving. "His eyes narrow" is yours to write. "He knows the kid never paid" is not, and neither is the same thought with the "he knows" filed off.
2356
2357
  - Do NOT repeat, rephrase, or re-ask anything you have already said (check the recent-activity history below). If you already asked a question and it hasn't been answered, wait for the answer -- do not ask it again in different words.
2357
2358
  - Silence is a valid response: if this event doesn't genuinely need words from you, do not include a say command at all.
2358
2359
  - Keep spoken lines SHORT -- one or two sentences. People don't monologue at strangers.
@@ -76,6 +76,41 @@ export function billAction(scene, actor, kind, label, opts = {}) {
76
76
  scene.updateStatus?.();
77
77
  return undefined;
78
78
  }
79
+ /**
80
+ * THE SAME QUESTION, ASKED WITHOUT PAYING -- "could I afford this?"
81
+ *
82
+ * GoEyGG9PSRM4mpXzG (Maka): "I shouldn't be hit on a simple/complex
83
+ * action if the command is rejected: 'Nothing on the grid answers to
84
+ * "camera" -- no host by that name, no PAN, no camera' ... should not
85
+ * count as a complex action."
86
+ *
87
+ * Exactly right, and the shape of the bug is worth naming because it is
88
+ * easy to write again: a verb that bills at the TOP of execute() has
89
+ * charged for the Action Phase before it knows whether it has a target.
90
+ * Typing a name the grid does not answer to then costs the same as an
91
+ * intrusion -- the player is told "nothing answers to that" and their
92
+ * turn is gone. Canon charges for an action TAKEN (p.163-167); a
93
+ * sentence the game refused to parse is not one.
94
+ *
95
+ * But a verb still wants to say "you have nothing left this phase"
96
+ * BEFORE it goes hunting for a target, or the player gets a page of
97
+ * resolution and a refusal at the end of it. So: ask with this at the
98
+ * top, spend with billAction at the point the action actually commits
99
+ * -- after every refusal, immediately before the roll or the delegation.
100
+ * The pair reads the same budget and returns the same wording, so the
101
+ * player cannot tell which one spoke.
102
+ */
103
+ export function actionRefusal(scene, actor, kind, opts = {}) {
104
+ const enc = encounterOf(scene, actor);
105
+ if (!enc || enc.ended)
106
+ return undefined;
107
+ if (!enc.isPhaseOf(actor))
108
+ return notYourPhase(enc, actor);
109
+ const budget = enc.budgetOf(actor);
110
+ if (!budget)
111
+ return notYourPhase(enc, actor);
112
+ return budget.refusalFor(kind, opts.attack === true);
113
+ }
79
114
  /**
80
115
  * A verb that is refused outright mid-fight unless it is your phase --
81
116
  * for things that are not actions at all but still cannot happen while
@@ -65,24 +65,39 @@ function parseData(raw) {
65
65
  return undefined;
66
66
  }
67
67
  }
68
+ /**
69
+ * File one, and SAY WHICH ONE (X3tZu4rspsa5CQW38, Maka: 'Let's change
70
+ * this to read "Logged - <ID>"').
71
+ *
72
+ * The id was always on the wire -- the route has answered
73
+ * `{ id, status, vetting }` since it was written -- and this function
74
+ * threw it away, returning a bare outcome word. So the only thing the
75
+ * player who filed a report could say about it afterwards was "I filed
76
+ * something", which is not enough to look it up, quote it, or ask about
77
+ * it. Returns the shape its siblings already use.
78
+ */
68
79
  export async function submitBacklog(text, meta) {
69
80
  const token = authToken();
70
81
  if (!token)
71
- return 'no-login';
82
+ return { outcome: 'no-login' };
72
83
  try {
73
84
  const res = await request('POST', `/${API_BASE}/item`, token, { text, ...meta });
74
- if (res.statusCode === 200)
75
- return 'ok';
85
+ if (res.statusCode === 200) {
86
+ // An id we cannot read is not a failure to file -- the item IS on
87
+ // the board. The caller falls back to wording that does not
88
+ // promise one rather than reporting a filing that worked as broken.
89
+ return { outcome: 'ok', id: parseData(res.data)?.id };
90
+ }
76
91
  if (res.statusCode === 401)
77
- return 'expired';
92
+ return { outcome: 'expired' };
78
93
  if (res.statusCode === 403)
79
- return 'forbidden';
94
+ return { outcome: 'forbidden' };
80
95
  if (res.statusCode === 400)
81
- return 'rejected';
82
- return 'offline';
96
+ return { outcome: 'rejected' };
97
+ return { outcome: 'offline' };
83
98
  }
84
99
  catch {
85
- return 'offline';
100
+ return { outcome: 'offline' };
86
101
  }
87
102
  }
88
103
  /**
@@ -121,6 +121,64 @@ const SELF_LOCATION = new RegExp([
121
121
  // "I'm standing/sitting/waiting in X".
122
122
  String.raw `\bI(?:'|’)?m\s+(?:stand|sitt|wait|post|hang|hold)\w*\s+(?:up\s+|out\s+|around\s+)*(?:at|in|by|outside)\s+(?:the\s+)?([^.,;!?"'’]{2,48})`,
123
123
  ].join('|'), 'i');
124
+ /**
125
+ * READING THE NPC'S MIND (BDYYpN9EZJtTJ5B6A, Maka: 'I shouldn't "notice"
126
+ * skitter's inner monolog: "Skitter's eyes narrow—sharp as a razor. The
127
+ * kid just walked off with product without settling up, and now he's
128
+ * shouting about it like it's some kinda joke."').
129
+ *
130
+ * A beat is what a person standing in the room can SEE and HEAR. The
131
+ * first sentence of that line is exactly that and is good writing. The
132
+ * second is Skitter's private assessment, printed to a player who has no
133
+ * way to know it -- and a player who reads an NPC's judgement of them
134
+ * cannot un-know it, which is half the game gone.
135
+ *
136
+ * WHAT THIS CATCHES AND WHAT IT CANNOT. The reported sentence carries no
137
+ * marker at all: it is free indirect discourse, a thought written as
138
+ * plain narration, and nothing short of a model could tell it from a
139
+ * statement of fact. So this rule does NOT claim to catch it -- the
140
+ * brief in models/npc.ts is what aims at that shape. What is catchable
141
+ * is interiority that ANNOUNCES itself: a mind named, or a mental act
142
+ * given as a fact.
143
+ *
144
+ * MEASURED, NOT GUESSED, over all 294 exposition beats the site has
145
+ * stored (GameSessionEvents, channel 'log'). Five lines match and all
146
+ * five are the bug:
147
+ *
148
+ * "...already sliding out of the booth's shadow in her mind if not
149
+ * her body."
150
+ * "...that's not natural, and Ilsen knows exactly what it means."
151
+ * "...but he's already running the math -- two of them, close
152
+ * quarters, his crew somewhere outside earshot."
153
+ * "Forty years on the pier means he knows exactly how far a man has
154
+ * to be standing before he can lie about why he's here."
155
+ * "The file updates itself quietly in his head."
156
+ *
157
+ * Nothing else in the corpus matches, and the near misses are the
158
+ * reason the patterns are this narrow. Good beats constantly describe
159
+ * inner life FROM THE OUTSIDE -- "something behind her eyes does the
160
+ * arithmetic", "the math of a fixer deciding whether she's being
161
+ * played", "like he's reading a bill of sale", "in a way that says
162
+ * she's heard this sound too many times". Every one of those is an
163
+ * observer's inference about visible behaviour, which is precisely what
164
+ * a beat is FOR. A pattern on the word "math" or "deciding" would eat
165
+ * all of them, so the anchors are the mind itself ("in his head", "in
166
+ * her mind"), certainty no observer could have ("knows exactly"), and
167
+ * the one idiom that always means silent calculation ("running the
168
+ * math").
169
+ */
170
+ const INTERIOR = new RegExp([
171
+ // The mind, named as a place something happens. "in his head", "in
172
+ // her mind", "inside their thoughts". NOT "in the back of the shop".
173
+ String.raw `\b(?:in|inside|through|across)\s+(?:his|her|their|its)\s+(?:mind|head|thoughts)\b`,
174
+ // A certainty no watcher could have. "he knows exactly what it
175
+ // means", "she knows exactly how far". The adverb is load-bearing:
176
+ // "he knows the code" is a fact about the world a beat may state.
177
+ String.raw `\b(?:knows|knew)\s+(?:exactly|full\s+well|perfectly\s+well|damn\s+well)\b`,
178
+ // Silent calculation as an event. "already running the math",
179
+ // "running the numbers in his head" (caught twice over, harmlessly).
180
+ String.raw `\brunning\s+the\s+(?:math|numbers|odds)\b`,
181
+ ].join('|'), 'i');
124
182
  /** Every place a self-location claim names in one line. */
125
183
  function placesClaimed(line) {
126
184
  const out = [];
@@ -268,6 +326,15 @@ export function judgeNarration(line, speaker, others, opts = {}) {
268
326
  };
269
327
  }
270
328
  }
329
+ // ALSO BEFORE the agency rules, and for the same reason: this is about
330
+ // the SPEAKER's own head, so there is no subject to resolve.
331
+ if (INTERIOR.test(t)) {
332
+ return {
333
+ refused: true,
334
+ subject: speaker,
335
+ note: `A BEAT IS WHAT THE ROOM CAN SEE AND HEAR. Your last beat read your own mind out loud -- what you know, what you are working out, what is going on in your head -- so it did not happen and the player was not shown it. Nobody standing in front of you can see any of that. Write the OUTSIDE of it instead: the look, the pause, the hand that stops moving, the glance at the door. A player who watches you narrow your eyes and go quiet learns more than one who is handed your conclusion.`,
336
+ };
337
+ }
271
338
  const contactAt = t.match(CONTACT);
272
339
  const movedAt = t.match(COMPELLED_MOVEMENT);
273
340
  if (!contactAt && !movedAt)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maka/maka-cli",
3
- "version": "5.212.0",
3
+ "version": "5.214.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.",