@mmnto/cli 2.0.0 → 2.1.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.
@@ -2946,22 +2946,33 @@ describe('buildPrePushHook — the review-leg floor arm (mmnto-ai/totem#2698)',
2946
2946
  // queued `merge-gate` verb (mmnto-ai/totem#2708) would satisfy it.
2947
2947
  expect(hook).not.toContain(`$TOTEM_CMD legs --help`);
2948
2948
  });
2949
- it('invokes the bare gate on strict and the advisory form otherwise', () => {
2949
+ it('invokes the bare gate on strict and the advisory form otherwise, recording the status on BOTH arms', () => {
2950
2950
  expect(hook).toContain('$TOTEM_CMD legs gate\n');
2951
- expect(hook).toContain('$TOTEM_CMD legs gate --advisory');
2952
- expect(hook).toContain('legs_status=$?');
2951
+ expect(hook).toContain('$TOTEM_CMD legs gate --advisory\n legs_status=$?');
2952
+ expect(hook).toContain('legs_strict=1\n $TOTEM_CMD legs gate\n legs_status=$?');
2953
2953
  });
2954
- it('blocks on exit 3 and on any other non-zero with DISTINCT lines', () => {
2955
- expect(hook).toContain(`echo "[Totem] BLOCKED: this push is legs-owed and carries no fresh falsification-leg deposit — run the leg, then 'totem legs deposit --sha HEAD --from <findings.json>' (mmnto-ai/totem#2698, strict mode)"`);
2956
- expect(hook).toContain('echo "[Totem] BLOCKED: the legs gate could not derive (totem legs gate exit status $legs_status) — fix the checkout and retry (strict mode)"');
2954
+ it('blocks on exit 3 and exit 2 at ANY tier with DISTINCT lines that name the knob; any other non-zero blocks on strict only (mmnto-ai/totem#2771)', () => {
2955
+ expect(hook).toContain(`echo "[Totem] BLOCKED: this push is legs-owed and carries no fresh falsification-leg deposit — run the leg, then 'totem legs deposit --sha HEAD --from <findings.json>' (mmnto-ai/totem#2698; strict mode, an agent seat, or hooks.legsOwed.enforce: block)"`);
2956
+ expect(hook).toContain('echo "[Totem] BLOCKED: the legs gate could not derive (totem legs gate exit status 2) — fix the checkout and retry (strict mode, an agent seat, or hooks.legsOwed.enforce: block)"');
2957
+ expect(hook).toContain('echo "[Totem] BLOCKED: the legs gate failed before deriving (totem legs gate exit status $legs_status) — fix the config or the CLI and retry (strict mode or an agent seat)"');
2957
2958
  expect(hook).toContain('if [ "$legs_status" = "3" ]; then');
2958
- expect(hook).toContain('elif [ "$legs_status" != "0" ]; then');
2959
+ expect(hook).toContain('elif [ "$legs_status" = "2" ]; then');
2960
+ expect(hook).toContain('elif [ "$legs_status" != "0" ] && [ "$legs_strict" = "1" ]; then');
2961
+ // The mapping sits AFTER both invocations, outside the tier guard: the
2962
+ // advisory arm falls through to the same three checks the strict arm does.
2963
+ const advisoryIdx = hook.indexOf('$TOTEM_CMD legs gate --advisory');
2964
+ const mappingIdx = hook.indexOf('if [ "$legs_status" = "3" ]; then');
2965
+ expect(advisoryIdx).toBeGreaterThan(-1);
2966
+ expect(mappingIdx).toBeGreaterThan(advisoryIdx);
2967
+ // And the hook never reads the config itself — the knob reaches it only as
2968
+ // an exit code (the gate is the one reader of hooks.legsOwed.enforce).
2969
+ expect(hook).not.toContain('totem.config');
2959
2970
  });
2960
2971
  it('FAILS CLOSED on strict when the resolved CLI lacks the verb, compat-opens otherwise', () => {
2961
2972
  // The OPPOSITE of the `--gate` / `--scope-to-diff` precedent, by ruling
2962
2973
  // (mmnto-ai/totem#2698 OQ2): those flags degrade to a form that still runs;
2963
2974
  // an absent VERB has no degraded form, so strict blocks with the cure.
2964
- expect(hook).toContain(`echo "[Totem] BLOCKED: this hook expects 'totem legs gate' (mmnto-ai/totem#2698) but the resolved CLI lacks it — 'npm i -g @mmnto/cli@latest' (strict mode)" >&2`);
2975
+ expect(hook).toContain(`echo "[Totem] BLOCKED: this hook expects 'totem legs gate' (mmnto-ai/totem#2698) but the resolved CLI lacks it — 'npm i -g @mmnto/cli@latest' (strict mode or an agent seat)" >&2`);
2965
2976
  expect(hook).toContain(`echo "[totem] Hook running without the legs gate (CLI predates 'totem legs'); 'npm i -g @mmnto/cli@latest' enables it." >&2`);
2966
2977
  });
2967
2978
  it('runs BEFORE the shield block and inside the $TOTEM_CMD guard', () => {
@@ -3119,8 +3130,8 @@ describe('the review-leg floor arm executed under sh (mmnto-ai/totem#2698)', ()
3119
3130
  expect(invocations()).not.toContain('legs gate --advisory');
3120
3131
  expect(r.status).toBe(1);
3121
3132
  });
3122
- it.skipIf(!shellOk)('standard tier, no agent: --advisory is passed and exit 3 does NOT block', () => {
3123
- writeStub({ helpText: HELP_WITH_GATE, gateExit: 3 });
3133
+ it.skipIf(!shellOk)('standard tier, no agent: --advisory is passed and exit 0 proceeds past the arm', () => {
3134
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 0 });
3124
3135
  const r = runHook({ tier: 'standard', agent: false });
3125
3136
  // Read from the recorded argv, not from the hook text: the flag has to
3126
3137
  // have reached the CLI, not merely appear in the template.
@@ -3129,6 +3140,44 @@ describe('the review-leg floor arm executed under sh (mmnto-ai/totem#2698)', ()
3129
3140
  expect(r.status).toBe(0);
3130
3141
  expect(r.stdout).not.toContain('BLOCKED');
3131
3142
  });
3143
+ // Since mmnto-ai/totem#2771 the advisory arm's exit code is READ. A real gate
3144
+ // under --advisory returns 3 or 2 only when hooks.legsOwed.enforce says
3145
+ // block, so a stub exiting 3 here IS the knob's shape as the hook sees it.
3146
+ it.skipIf(!shellOk)('standard tier + gate exit 3 (the enforce: block shape): BLOCKS with the line that names the knob', () => {
3147
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 3 });
3148
+ const r = runHook({ tier: 'standard', agent: false });
3149
+ expect(invocations()).toContain('legs gate --advisory');
3150
+ expect(r.status).toBe(1);
3151
+ expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed');
3152
+ expect(r.stdout).toContain('strict mode, an agent seat, or hooks.legsOwed.enforce: block');
3153
+ expect(r.stdout).toContain('[Totem] legs: stub gate line');
3154
+ });
3155
+ it.skipIf(!shellOk)('standard tier + gate exit 2: blocks with the DISTINCT not-derived line', () => {
3156
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 2 });
3157
+ const r = runHook({ tier: 'standard', agent: false });
3158
+ expect(invocations()).toContain('legs gate --advisory');
3159
+ expect(r.status).toBe(1);
3160
+ expect(r.stdout).toContain('the legs gate could not derive (totem legs gate exit status 2)');
3161
+ expect(r.stdout).not.toContain('this push is legs-owed');
3162
+ });
3163
+ // A PIN, not a falsifier: this case holds on the pre-knob hook too (whose
3164
+ // standard arm never read the status at all). It is here so the one
3165
+ // behaviour the knob must NOT change stays asserted next to the ones it does.
3166
+ it.skipIf(!shellOk)('standard tier + gate exit 1 (a failure BEFORE the derivation): passes, byte for byte the pre-knob behaviour', () => {
3167
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 1 });
3168
+ const r = runHook({ tier: 'standard', agent: false });
3169
+ expect(invocations()).toContain('legs gate --advisory');
3170
+ expect(r.status).toBe(0);
3171
+ expect(r.stdout).not.toContain('BLOCKED');
3172
+ });
3173
+ it.skipIf(!shellOk)('strict tier + gate exit 1: blocks with the failed-before-deriving line, not the not-derived one', () => {
3174
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 1 });
3175
+ const r = runHook({ tier: 'strict', agent: false });
3176
+ expect(invocations()).toContain('legs gate');
3177
+ expect(r.status).toBe(1);
3178
+ expect(r.stdout).toContain('the legs gate failed before deriving (totem legs gate exit status 1)');
3179
+ expect(r.stdout).not.toContain('could not derive');
3180
+ });
3132
3181
  });
3133
3182
  // The arm COMPOSED with the real gate (mmnto-ai/totem#2698 fold 2, MATERIAL).
3134
3183
  //
@@ -3247,8 +3296,8 @@ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2
3247
3296
  process.chdir(realCwd);
3248
3297
  cleanTmpDir(tmpDir);
3249
3298
  });
3250
- function runHook() {
3251
- fs.writeFileSync(path.join(repoDir, 'pre-push'), buildPrePushHook({ ...RENDER, tier: 'strict' }));
3299
+ function runHook(tier = 'strict') {
3300
+ fs.writeFileSync(path.join(repoDir, 'pre-push'), buildPrePushHook({ ...RENDER, tier }));
3252
3301
  const env = {
3253
3302
  ...process.env,
3254
3303
  PATH: [binDir, process.env['PATH'] ?? ''].join(path.delimiter),
@@ -3259,6 +3308,20 @@ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2
3259
3308
  const result = spawnSync('sh', ['./pre-push'], { cwd: repoDir, env, encoding: 'utf-8' });
3260
3309
  return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
3261
3310
  }
3311
+ /**
3312
+ * Rewrite the fixture's config with the knob — in the WORKING TREE only. The
3313
+ * gate judges the committed branch diff, so an uncommitted config edit never
3314
+ * enters the owed set; the gate's config LOAD reads the working file.
3315
+ */
3316
+ function setEnforce(enforce) {
3317
+ fs.writeFileSync(path.join(repoDir, 'totem.config.ts'), [
3318
+ 'export default {',
3319
+ " targets: [{ glob: 'docs/**/*.md', type: 'spec', strategy: 'markdown-heading' }],",
3320
+ ` hooks: { legsOwed: { globs: ['docs/**'], enforce: '${enforce}' } },`,
3321
+ '};',
3322
+ '',
3323
+ ].join(NL));
3324
+ }
3262
3325
  const invocations = () => {
3263
3326
  const native = logPath.split('/').join(path.sep);
3264
3327
  return fs.existsSync(native) ? fs.readFileSync(native, 'utf-8').trim().split(NL) : [];
@@ -3374,6 +3437,391 @@ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2
3374
3437
  expect(r.stdout).toContain('[Totem] legs: not owed — no changed path matched hooks.legsOwed.globs (1 globs; head');
3375
3438
  expect(r.stdout).not.toContain('BLOCKED');
3376
3439
  });
3440
+ // The knob, end to end (mmnto-ai/totem#2771): the config declares it, the
3441
+ // REAL gate reads it and maps the exit, the STANDARD-tier hook — which passes
3442
+ // --advisory and, before the knob, ignored the status entirely — blocks on
3443
+ // what comes back. No stub scripts the 3 here; the derivation produces it.
3444
+ it.skipIf(!composedOk)("OWED, no deposit, STANDARD tier, hooks.legsOwed.enforce: 'block': the hook blocks and names the knob", () => {
3445
+ setEnforce('block');
3446
+ const r = runHook('standard');
3447
+ // The advisory FLAG was passed — the tier did not change — and the gate
3448
+ // still returned 3 because the knob overrode it.
3449
+ expect(invocations()).toContain('legs gate --advisory');
3450
+ expect(invocations()).not.toContain('legs gate');
3451
+ expect(r.status).toBe(1);
3452
+ expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed (docs/** →');
3453
+ expect(r.stdout).toContain('[Totem] legs: hooks.legsOwed.enforce = block');
3454
+ expect(r.stdout).toContain('strict mode, an agent seat, or hooks.legsOwed.enforce: block');
3455
+ // The shield block after the arm was never reached.
3456
+ expect(invocations()).not.toContain('doctor --strict');
3457
+ });
3458
+ it.skipIf(!composedOk)("OWED, no deposit, STRICT tier, hooks.legsOwed.enforce: 'advisory': the gate's text prints, the hook passes", () => {
3459
+ setEnforce('advisory');
3460
+ const r = runHook('strict');
3461
+ expect(invocations()).toContain('legs gate');
3462
+ // The gate's own verdict text is byte-identical to the blocking case —
3463
+ // only the exit code moved — and the knob line says why.
3464
+ expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed (docs/** →');
3465
+ expect(r.stdout).toContain('[Totem] legs: hooks.legsOwed.enforce = advisory');
3466
+ // The HOOK's block line never fires, and the hook proceeds to the shield block.
3467
+ expect(r.stdout).not.toContain('strict mode, an agent seat, or hooks.legsOwed.enforce: block');
3468
+ expect(invocations()).toContain('doctor --strict');
3469
+ expect(r.status).toBe(0);
3470
+ });
3471
+ it.skipIf(!composedOk)("OWED with a covering ANCESTOR deposit under enforce: 'block' on the STANDARD tier: the hook passes on evidence", () => {
3472
+ setEnforce('block');
3473
+ deposit(ancestorSha);
3474
+ const r = runHook('standard');
3475
+ expect(invocations()).toContain('legs gate --advisory');
3476
+ expect(r.stdout).toContain('[Totem] legs evidence: .totem/artifacts/legs/');
3477
+ expect(r.stdout).toContain('[Totem] legs: hooks.legsOwed.enforce = block');
3478
+ expect(r.stdout).not.toContain('BLOCKED');
3479
+ expect(r.status).toBe(0);
3480
+ });
3481
+ });
3482
+ // ─── Fences and bare hash lines (mmnto-ai/totem#2769) ─────
3483
+ //
3484
+ // Two holes the mmnto-ai/totem#2759 round declined as pre-existing. Each
3485
+ // NEGATIVE case here passes on the reader as it stood on main (a lone `####`
3486
+ // counted as body; a promised heading inside a fenced block matched); each must
3487
+ // BLOCK after. The POSITIVE cases pin what the fix must not take with it: a
3488
+ // section whose only content is a code block is a written section, and a
3489
+ // fenced quote of a heading beside a real section changes nothing.
3490
+ describe('buildPreCommitHook spec-gate reader — fences and bare hash lines (mmnto-ai/totem#2769)', () => {
3491
+ const shellOk = spawnSync('sh', ['-c', 'command -v node >/dev/null 2>&1'], { encoding: 'utf-8' }).status === 0;
3492
+ const PROMISED = SPEC_SYSTEM_PROMPT.split('\n').filter((line) => line.startsWith('### '));
3493
+ function promised(prefix) {
3494
+ const found = PROMISED.find((heading) => heading.startsWith(prefix));
3495
+ if (found === undefined)
3496
+ throw new Error(`no promised heading starts with ${prefix}`);
3497
+ return found;
3498
+ }
3499
+ const EDGE_CASES = promised('### Edge Cases');
3500
+ const EXECUTION_FLOW = promised('### Execution Flow');
3501
+ const TEST_PLAN = promised('### Test Plan');
3502
+ /** Three backticks, built rather than authored (three quoting layers sit between here and the reader). */
3503
+ const FENCE = String.fromCharCode(96, 96, 96);
3504
+ const NL = String.fromCharCode(10);
3505
+ function nineBodied(overrides = {}) {
3506
+ return PROMISED.map((heading) => {
3507
+ const override = overrides[heading];
3508
+ return override === undefined
3509
+ ? `${heading}${NL}${NL}A non-blank body under ${heading}.${NL}`
3510
+ : override;
3511
+ })
3512
+ .filter((block) => block.length > 0)
3513
+ .join(NL);
3514
+ }
3515
+ let tmpDir;
3516
+ beforeEach(() => {
3517
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-hook-2769-'));
3518
+ execSync('git init -q', { cwd: tmpDir, stdio: 'ignore' });
3519
+ execSync('git checkout -q -b feat/fence-suite', { cwd: tmpDir, stdio: 'ignore' });
3520
+ fs.writeFileSync(path.join(tmpDir, 'pre-commit'), buildPreCommitHook(RENDER));
3521
+ });
3522
+ afterEach(() => {
3523
+ cleanTmpDir(tmpDir);
3524
+ });
3525
+ function judge(content) {
3526
+ const dir = path.join(tmpDir, '.totem', 'artifacts', 'runs');
3527
+ fs.mkdirSync(dir, { recursive: true });
3528
+ fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({ output: { content } }), null, 2));
3529
+ const r = spawnSync('sh', ['./pre-commit'], {
3530
+ cwd: tmpDir,
3531
+ encoding: 'utf-8',
3532
+ env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
3533
+ });
3534
+ return { status: r.status, stdout: r.stdout };
3535
+ }
3536
+ // ── Negative: pass today, must block ──
3537
+ it.skipIf(!shellOk)('a lone #### line is not a body (hole 1)', () => {
3538
+ const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}####${NL}` }));
3539
+ expect(r.status).toBe(1);
3540
+ expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
3541
+ });
3542
+ it.skipIf(!shellOk)('a run of # characters with only whitespace around it is not a body either', () => {
3543
+ const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL} ### ${NL}#${NL}` }));
3544
+ expect(r.status).toBe(1);
3545
+ expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
3546
+ });
3547
+ it.skipIf(!shellOk)('the promised skeleton quoted inside a fenced block is not the sections it names (hole 2)', () => {
3548
+ const r = judge(`Some prose about the draft.${NL}${NL}${FENCE}${NL}${nineBodied()}${NL}${FENCE}${NL}`);
3549
+ expect(r.status).toBe(1);
3550
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
3551
+ });
3552
+ it.skipIf(!shellOk)('a tilde fence hides a heading the same way', () => {
3553
+ const r = judge(nineBodied({ [TEST_PLAN]: `~~~${NL}${TEST_PLAN}${NL}${NL}quoted, not written${NL}~~~${NL}` }));
3554
+ expect(r.status).toBe(1);
3555
+ expect(r.stdout).toContain(`is missing heading ${TEST_PLAN}`);
3556
+ });
3557
+ it.skipIf(!shellOk)('a heading whose only content is an EMPTY fence is empty', () => {
3558
+ const r = judge(nineBodied({ [EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}${NL}${FENCE}${NL}` }));
3559
+ expect(r.status).toBe(1);
3560
+ expect(r.stdout).toContain(`has an empty heading ${EXECUTION_FLOW}`);
3561
+ });
3562
+ // ── Positive: must keep passing ──
3563
+ it.skipIf(!shellOk)('a section whose only content is a code block is a written section', () => {
3564
+ const r = judge(nineBodied({
3565
+ [EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}mermaid${NL}A --> B${NL}${FENCE}${NL}`,
3566
+ }));
3567
+ expect(r.status).toBe(0);
3568
+ expect(r.stdout).not.toContain('BLOCKED');
3569
+ });
3570
+ it.skipIf(!shellOk)('a fenced quote of a heading beside the real section changes nothing', () => {
3571
+ const r = judge(nineBodied({
3572
+ [TEST_PLAN]: `${TEST_PLAN}${NL}${NL}Real plan text.${NL}${NL}${FENCE}${NL}${EDGE_CASES}${NL}(quoted as an example)${NL}${FENCE}${NL}`,
3573
+ }));
3574
+ expect(r.status).toBe(0);
3575
+ expect(r.stdout).not.toContain('BLOCKED');
3576
+ expect(r.stdout).not.toContain('tolerated');
3577
+ });
3578
+ it.skipIf(!shellOk)('a heading that appears fenced first and real later is found at the real line — the disclosure proves which', () => {
3579
+ // The EXACT promised heading sits inside a fence; the real section below
3580
+ // is written in the TOLERANT form. On the pre-fix reader the exact pass
3581
+ // matched the fenced copy and the pass line carried no tolerance; skipping
3582
+ // the fenced copy makes the tolerant pass find the real line and SAY so.
3583
+ const r = judge(nineBodied({
3584
+ [EXECUTION_FLOW]: `${FENCE}${NL}${EXECUTION_FLOW}${NL}${FENCE}${NL}${NL}### Execution Flow${NL}${NL}The real section, in the tolerant spelling.${NL}`,
3585
+ }));
3586
+ expect(r.status).toBe(0);
3587
+ expect(r.stdout).not.toContain('BLOCKED');
3588
+ expect(r.stdout).toContain(`tolerated ${EXECUTION_FLOW} ~ ### Execution Flow`);
3589
+ });
3590
+ it.skipIf(!shellOk)('an unclosed fence runs to the end of the draft, and the block says where it opened', () => {
3591
+ // Everything after an unclosed opener is inside the fence: the headings
3592
+ // quoted there are not candidates, so the draft is missing them — and the
3593
+ // reason names the fence, so the cure is never a heading the draft contains.
3594
+ const r = judge(`Intro.${NL}${FENCE}${NL}${nineBodied()}`);
3595
+ expect(r.status).toBe(1);
3596
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
3597
+ expect(r.stdout).toContain('it appears only inside a fenced code block opened at line 2');
3598
+ expect(r.stdout).toContain('that fence is never closed');
3599
+ });
3600
+ // ── The fence state's edges (the leg's probes, pinned) ──
3601
+ it.skipIf(!shellOk)('a three-backtick line does not close a four-backtick fence', () => {
3602
+ // CommonMark: the closer is the same character, at least as long. A draft
3603
+ // wrapped in a four-backtick fence with one nested three-backtick example
3604
+ // is one code block with zero headings.
3605
+ const FENCE4 = FENCE + String.fromCharCode(96);
3606
+ const r = judge(`Intro prose.${NL}${FENCE4}${NL}quoted example:${NL}${FENCE}${NL}${nineBodied()}${NL}${FENCE4}${NL}`);
3607
+ expect(r.status).toBe(1);
3608
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
3609
+ expect(r.stdout).toContain('inside a fenced code block opened at line 2');
3610
+ });
3611
+ it.skipIf(!shellOk)('a four-backtick closer closes a three-backtick fence', () => {
3612
+ const FENCE4 = FENCE + String.fromCharCode(96);
3613
+ const r = judge(nineBodied({
3614
+ [EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}${NL}A --> B${NL}${FENCE4}${NL}`,
3615
+ }));
3616
+ expect(r.status).toBe(0);
3617
+ expect(r.stdout).not.toContain('BLOCKED');
3618
+ });
3619
+ it.skipIf(!shellOk)('a fence line indented four spaces is not a fence (an indented code block, or a lazy continuation after a paragraph)', () => {
3620
+ // CommonMark: up to three spaces of indentation keeps a fence a fence; four
3621
+ // makes it an indented code block. The DISCRIMINATING shape: an indented
3622
+ // fence line with the whole skeleton after it and no closer. A reader that
3623
+ // treated the indented line as an opener would swallow every heading; this
3624
+ // one reads it as text, so the draft passes.
3625
+ const r = judge(`Intro.${NL} ${FENCE}${NL}${nineBodied()}`);
3626
+ expect(r.status).toBe(0);
3627
+ expect(r.stdout).not.toContain('BLOCKED');
3628
+ });
3629
+ it.skipIf(!shellOk)('a backtick opener whose info string carries a backtick is not a fence', () => {
3630
+ const r = judge(nineBodied({
3631
+ [EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}js${String.fromCharCode(96)}x${NL}`,
3632
+ }));
3633
+ expect(r.status).toBe(0);
3634
+ expect(r.stdout).not.toContain('BLOCKED');
3635
+ });
3636
+ it.skipIf(!shellOk)('a run of tildes used as a divider opens a fence in Markdown too, and the block says so', () => {
3637
+ // Twenty tildes are a fence opener under CommonMark (a thematic break is
3638
+ // ---, *** or ___); every heading after it is code until a closer at
3639
+ // least as long. The block names the fence rather than a heading the
3640
+ // draft contains verbatim.
3641
+ const rule = '~'.repeat(20);
3642
+ const r = judge(nineBodied({ [PROMISED[1]]: `${PROMISED[1]}${NL}${NL}Body.${NL}${NL}${rule}${NL}` }));
3643
+ expect(r.status).toBe(1);
3644
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[2]}`);
3645
+ // Line 9: three lines for the first section, one blank, then the override's
3646
+ // heading, blank, body, blank, and the divider.
3647
+ expect(r.stdout).toContain('it appears only inside a fenced code block opened at line 9');
3648
+ expect(r.stdout).toContain('that fence is never closed');
3649
+ });
3650
+ it.skipIf(!shellOk)('a fenced promised heading in its TOLERANT spelling is not body either', () => {
3651
+ // The matcher treats the parenthetical-dropped spelling as the same heading;
3652
+ // the body rule now does too, whatever the indentation inside the fence.
3653
+ const tolerant = EXECUTION_FLOW.replace(/ \([^)]*\)$/, '');
3654
+ expect(tolerant).not.toBe(EXECUTION_FLOW);
3655
+ const r = judge(nineBodied({
3656
+ [EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL} ${tolerant}${NL}${FENCE}${NL}`,
3657
+ }));
3658
+ expect(r.status).toBe(1);
3659
+ expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
3660
+ expect(r.stdout).toContain('nothing under it counts as body: a promised heading quoted inside a fenced code block');
3661
+ });
3662
+ it.skipIf(!shellOk)('an INDENTED fenced copy of a missing heading is still found and named', () => {
3663
+ const indented = nineBodied()
3664
+ .split(NL)
3665
+ .map((line) => ` ${line}`)
3666
+ .join(NL);
3667
+ const r = judge(`Prose.${NL}${FENCE}${NL}${indented}${NL}${FENCE}${NL}`);
3668
+ expect(r.status).toBe(1);
3669
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
3670
+ expect(r.stdout).toContain('it appears only inside a fenced code block opened at line 2');
3671
+ // Closed fence: the cure is to write the section outside it, not to close it.
3672
+ expect(r.stdout).toContain('write the section outside the fence');
3673
+ expect(r.stdout).not.toContain('never closed');
3674
+ });
3675
+ it.skipIf(!shellOk)('a lone #### under a heading names the rule that emptied the section', () => {
3676
+ const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}####${NL}` }));
3677
+ expect(r.status).toBe(1);
3678
+ expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES} — nothing under it counts as body: a line of # characters`);
3679
+ });
3680
+ it.skipIf(!shellOk)('every rule that emptied a section is named, and an empty fence pair is one of them', () => {
3681
+ // Both rules at once: a bare hash line and a fenced promised heading.
3682
+ const both = judge(nineBodied({
3683
+ [EDGE_CASES]: `${EDGE_CASES}${NL}####${NL}${FENCE}${NL}${TEST_PLAN}${NL}${FENCE}${NL}`,
3684
+ }));
3685
+ expect(both.status).toBe(1);
3686
+ expect(both.stdout).toContain('nothing under it counts as body: a promised heading quoted inside a fenced code block; a line of # characters');
3687
+ // An empty fence pair is a rule this slice introduced; it names itself too.
3688
+ const empty = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL}${FENCE}${NL}` }));
3689
+ expect(empty.status).toBe(1);
3690
+ expect(empty.stdout).toContain('nothing under it counts as body: an empty fenced code block');
3691
+ });
3692
+ it.skipIf(!shellOk)('a closing fence that repeats the info string does not close (the cure names a bare closer)', () => {
3693
+ // A common model output error: ```mermaid … ```mermaid. Under CommonMark the
3694
+ // second line is not a closer, so every heading after it is fenced.
3695
+ const idx = PROMISED.indexOf(EXECUTION_FLOW);
3696
+ const r = judge(nineBodied({
3697
+ [EXECUTION_FLOW]: `${EXECUTION_FLOW}${NL}${FENCE}mermaid${NL}A --> B${NL}${FENCE}mermaid${NL}`,
3698
+ }));
3699
+ expect(r.status).toBe(1);
3700
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[idx + 1]}`);
3701
+ expect(r.stdout).toContain('close it with a bare closing fence');
3702
+ });
3703
+ it.skipIf(!shellOk)('the never-closed cure is offered only for the fence the copy sits in', () => {
3704
+ // Test Plan exists only as a copy inside a CLOSED fence; an unrelated fence
3705
+ // is left open at the end of the draft. The cure must not blame that one.
3706
+ const r = judge(`${nineBodied({
3707
+ [EDGE_CASES]: `${EDGE_CASES}${NL}${NL}A real body line.${NL}${NL}${FENCE}${NL}${TEST_PLAN}${NL}${FENCE}${NL}`,
3708
+ [TEST_PLAN]: '',
3709
+ })}${NL}${FENCE}${NL}stray, never closed${NL}`);
3710
+ expect(r.status).toBe(1);
3711
+ expect(r.stdout).toContain(`is missing heading ${TEST_PLAN}`);
3712
+ expect(r.stdout).toContain('write the section outside the fence');
3713
+ expect(r.stdout).not.toContain('never closed');
3714
+ });
3715
+ it.skipIf(!shellOk)('RECORD: a bound record whose only headings sit inside a fence has no heading with a body', () => {
3716
+ const abs = path.join(tmpDir, '.totem', 'specs', 'rec.md');
3717
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
3718
+ fs.writeFileSync(abs, `${FENCE}yaml${NL}# Problem${NL}problem: the thing${NL}${FENCE}${NL}`, 'utf-8');
3719
+ const sha256 = crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex');
3720
+ const dir = path.join(tmpDir, '.totem', 'artifacts', 'runs');
3721
+ fs.mkdirSync(dir, { recursive: true });
3722
+ fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({
3723
+ grounding: {
3724
+ anchor: { kind: GROUNDING_ANCHOR_RECORD, ref: '.totem/specs/rec.md', sha256 },
3725
+ },
3726
+ }), null, 2));
3727
+ const r = spawnSync('sh', ['./pre-commit'], {
3728
+ cwd: tmpDir,
3729
+ encoding: 'utf-8',
3730
+ env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
3731
+ });
3732
+ expect(r.status).toBe(1);
3733
+ expect(r.stdout).toContain('the bound record at .totem/specs/rec.md has no heading with a body');
3734
+ // A real heading above the fence cures it — the record is edited, not re-run.
3735
+ fs.writeFileSync(abs, `# Record${NL}${NL}${FENCE}yaml${NL}problem: the thing${NL}${FENCE}${NL}`, 'utf-8');
3736
+ const sha2 = crypto.createHash('sha256').update(fs.readFileSync(abs)).digest('hex');
3737
+ fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({
3738
+ grounding: {
3739
+ anchor: { kind: GROUNDING_ANCHOR_RECORD, ref: '.totem/specs/rec.md', sha256: sha2 },
3740
+ },
3741
+ }), null, 2));
3742
+ const ok = spawnSync('sh', ['./pre-commit'], {
3743
+ cwd: tmpDir,
3744
+ encoding: 'utf-8',
3745
+ env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
3746
+ });
3747
+ expect(ok.status).toBe(0);
3748
+ expect(ok.stdout).toContain('record sha256 matches');
3749
+ });
3750
+ it.skipIf(!shellOk)('#### inside a fence is code, and code is body', () => {
3751
+ const r = judge(nineBodied({
3752
+ [EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL}####${NL}#####${NL}${FENCE}${NL}`,
3753
+ }));
3754
+ expect(r.status).toBe(0);
3755
+ expect(r.stdout).not.toContain('BLOCKED');
3756
+ });
3757
+ it.skipIf(!shellOk)('seven hashes are a bare hash line, not a heading and not a body', () => {
3758
+ const r = judge(nineBodied({ [EDGE_CASES]: `${EDGE_CASES}${NL}#######${NL}` }));
3759
+ expect(r.status).toBe(1);
3760
+ expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
3761
+ });
3762
+ it.skipIf(!shellOk)('a fenced promised heading is not body for the section above it', () => {
3763
+ // Zero authored words: the only content under Edge Cases is the next
3764
+ // promised heading, quoted. Not a written section.
3765
+ const r = judge(nineBodied({
3766
+ [EDGE_CASES]: `${EDGE_CASES}${NL}${FENCE}${NL}${PROMISED[PROMISED.indexOf(EDGE_CASES) + 1]}${NL}${FENCE}${NL}`,
3767
+ }));
3768
+ expect(r.status).toBe(1);
3769
+ expect(r.stdout).toContain(`has an empty heading ${EDGE_CASES}`);
3770
+ });
3771
+ it.skipIf(!shellOk)('a closer followed by a non-breaking space does not close the fence', () => {
3772
+ // CommonMark permits only spaces and tabs after a closing fence; JS trim()
3773
+ // would accept U+00A0 and close a fence Markdown keeps open, letting the
3774
+ // skeleton after it reach the gate.
3775
+ const NBSP = String.fromCharCode(160);
3776
+ const r = judge(`Intro.${NL}${FENCE}${NL}quoted${NL}${FENCE}${NBSP}${NL}${nineBodied()}`);
3777
+ expect(r.status).toBe(1);
3778
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
3779
+ expect(r.stdout).toContain('that fence is never closed');
3780
+ // A closer followed by ordinary spaces and a tab still closes.
3781
+ const TAB = String.fromCharCode(9);
3782
+ const ok = judge(`Intro.${NL}${FENCE}${NL}quoted${NL}${FENCE} ${TAB}${NL}${NL}${nineBodied()}`);
3783
+ expect(ok.status).toBe(0);
3784
+ });
3785
+ it.skipIf(!shellOk)('a matching delimiter indented four spaces does not close the fence', () => {
3786
+ // CommonMark: a closing fence may be indented at most three spaces. The
3787
+ // reader counts the run only when the indent is under four, so a
3788
+ // four-space delimiter is text inside the fence and the skeleton after it
3789
+ // stays fenced (Greptile follow-up on mmnto-ai/totem#2769).
3790
+ const r = judge(`Intro.${NL}${FENCE}${NL}quoted${NL} ${FENCE}${NL}${nineBodied()}`);
3791
+ expect(r.status).toBe(1);
3792
+ expect(r.stdout).toContain(`is missing heading ${PROMISED[0]}`);
3793
+ expect(r.stdout).toContain('that fence is never closed');
3794
+ // Three spaces still close.
3795
+ const ok = judge(`Intro.${NL}${FENCE}${NL}quoted${NL} ${FENCE}${NL}${NL}${nineBodied()}`);
3796
+ expect(ok.status).toBe(0);
3797
+ });
3798
+ // ── The DOCUMENT arm (overridden prompt, bound record) under fences ──
3799
+ function judgeDocument(content) {
3800
+ const dir = path.join(tmpDir, '.totem', 'artifacts', 'runs');
3801
+ fs.mkdirSync(dir, { recursive: true });
3802
+ fs.writeFileSync(path.join(dir, 'draft.json'), JSON.stringify(specEvidenceArtifact({
3803
+ admission: { runMetadata: { caller: 'spec', promptSource: PROMPT_SOURCE_OVERRIDE } },
3804
+ output: { content },
3805
+ }), null, 2));
3806
+ const r = spawnSync('sh', ['./pre-commit'], {
3807
+ cwd: tmpDir,
3808
+ encoding: 'utf-8',
3809
+ env: { ...process.env, CLAUDE_CODE_AGENT: '1' },
3810
+ });
3811
+ return { status: r.status, stdout: r.stdout };
3812
+ }
3813
+ it.skipIf(!shellOk)('DOCUMENT: a heading that exists only inside a fence is no heading', () => {
3814
+ // A YAML document whose "# Problem" lines sit inside a fence has no
3815
+ // markdown heading at all; the pre-fix reader read the comments as headings.
3816
+ const r = judgeDocument(`The spec, as YAML:${NL}${NL}${FENCE}yaml${NL}# Problem${NL}problem: the thing${NL}# Tasks${NL}tasks: [a, b]${NL}${FENCE}${NL}`);
3817
+ expect(r.status).toBe(1);
3818
+ expect(r.stdout).toContain('has no heading with a body');
3819
+ });
3820
+ it.skipIf(!shellOk)('DOCUMENT: a real heading whose body is a code block is bodied', () => {
3821
+ const r = judgeDocument(`## The spec${NL}${NL}${FENCE}yaml${NL}problem: the thing${NL}${FENCE}${NL}`);
3822
+ expect(r.status).toBe(0);
3823
+ expect(r.stdout).toContain('shape DOCUMENT');
3824
+ });
3377
3825
  });
3378
3826
  // ─── The frozen mutation suite (mmnto-ai/totem#2737) ─────
3379
3827
  //