@mmnto/cli 1.122.0 → 1.124.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.
Files changed (85) hide show
  1. package/dist/commands/config-drift.test.js +73 -1
  2. package/dist/commands/config-drift.test.js.map +1 -1
  3. package/dist/commands/extract-shared.d.ts.map +1 -1
  4. package/dist/commands/extract-shared.js +2 -6
  5. package/dist/commands/extract-shared.js.map +1 -1
  6. package/dist/commands/extract.test.js +19 -1
  7. package/dist/commands/extract.test.js.map +1 -1
  8. package/dist/commands/init-templates.d.ts +6 -6
  9. package/dist/commands/init-templates.d.ts.map +1 -1
  10. package/dist/commands/init-templates.js +8 -7
  11. package/dist/commands/init-templates.js.map +1 -1
  12. package/dist/commands/init.test.js +18 -5
  13. package/dist/commands/init.test.js.map +1 -1
  14. package/dist/commands/install-hooks.d.ts.map +1 -1
  15. package/dist/commands/install-hooks.js +58 -0
  16. package/dist/commands/install-hooks.js.map +1 -1
  17. package/dist/commands/install-hooks.test.js +460 -5
  18. package/dist/commands/install-hooks.test.js.map +1 -1
  19. package/dist/commands/legs.d.ts +259 -0
  20. package/dist/commands/legs.d.ts.map +1 -0
  21. package/dist/commands/legs.js +623 -0
  22. package/dist/commands/legs.js.map +1 -0
  23. package/dist/commands/legs.test.d.ts +20 -0
  24. package/dist/commands/legs.test.d.ts.map +1 -0
  25. package/dist/commands/legs.test.js +1004 -0
  26. package/dist/commands/legs.test.js.map +1 -0
  27. package/dist/commands/pre-push-gate-matrix.test.js +14 -0
  28. package/dist/commands/pre-push-gate-matrix.test.js.map +1 -1
  29. package/dist/commands/review-fan.d.ts +40 -1
  30. package/dist/commands/review-fan.d.ts.map +1 -1
  31. package/dist/commands/review-fan.js +108 -3
  32. package/dist/commands/review-fan.js.map +1 -1
  33. package/dist/commands/review-fan.test.js +444 -6
  34. package/dist/commands/review-fan.test.js.map +1 -1
  35. package/dist/commands/review-learn.d.ts +3 -0
  36. package/dist/commands/review-learn.d.ts.map +1 -1
  37. package/dist/commands/review-learn.js +4 -7
  38. package/dist/commands/review-learn.js.map +1 -1
  39. package/dist/commands/review-learn.test.js +19 -2
  40. package/dist/commands/review-learn.test.js.map +1 -1
  41. package/dist/commands/shield-covariate.test.js +19 -2
  42. package/dist/commands/shield-covariate.test.js.map +1 -1
  43. package/dist/commands/shield-learn.test.js +27 -5
  44. package/dist/commands/shield-learn.test.js.map +1 -1
  45. package/dist/commands/shield-nonreview.test.js +140 -1
  46. package/dist/commands/shield-nonreview.test.js.map +1 -1
  47. package/dist/commands/shield-retrieve.test.d.ts +2 -0
  48. package/dist/commands/shield-retrieve.test.d.ts.map +1 -0
  49. package/dist/commands/shield-retrieve.test.js +91 -0
  50. package/dist/commands/shield-retrieve.test.js.map +1 -0
  51. package/dist/commands/shield-templates.d.ts +7 -0
  52. package/dist/commands/shield-templates.d.ts.map +1 -1
  53. package/dist/commands/shield-templates.js +7 -0
  54. package/dist/commands/shield-templates.js.map +1 -1
  55. package/dist/commands/shield.d.ts +3 -1
  56. package/dist/commands/shield.d.ts.map +1 -1
  57. package/dist/commands/shield.js +70 -10
  58. package/dist/commands/shield.js.map +1 -1
  59. package/dist/commands/spec.d.ts +29 -4
  60. package/dist/commands/spec.d.ts.map +1 -1
  61. package/dist/commands/spec.js +51 -13
  62. package/dist/commands/spec.js.map +1 -1
  63. package/dist/commands/spec.test.js +221 -15
  64. package/dist/commands/spec.test.js.map +1 -1
  65. package/dist/commands/triage-retrieve.test.d.ts +2 -0
  66. package/dist/commands/triage-retrieve.test.d.ts.map +1 -0
  67. package/dist/commands/triage-retrieve.test.js +90 -0
  68. package/dist/commands/triage-retrieve.test.js.map +1 -0
  69. package/dist/commands/triage.d.ts +14 -1
  70. package/dist/commands/triage.d.ts.map +1 -1
  71. package/dist/commands/triage.js +21 -6
  72. package/dist/commands/triage.js.map +1 -1
  73. package/dist/git.d.ts +19 -0
  74. package/dist/git.d.ts.map +1 -1
  75. package/dist/git.js +13 -4
  76. package/dist/git.js.map +1 -1
  77. package/dist/index.js +40 -0
  78. package/dist/index.js.map +1 -1
  79. package/dist/utils.d.ts +5 -5
  80. package/dist/utils.d.ts.map +1 -1
  81. package/dist/utils.js +5 -6
  82. package/dist/utils.js.map +1 -1
  83. package/dist/utils.test.js +42 -48
  84. package/dist/utils.test.js.map +1 -1
  85. package/package.json +2 -2
@@ -1,8 +1,9 @@
1
- import { execSync, spawnSync } from 'node:child_process';
1
+ import { execFileSync, execSync, spawnSync } from 'node:child_process';
2
2
  import * as crypto from 'node:crypto';
3
3
  import * as fs from 'node:fs';
4
4
  import * as os from 'node:os';
5
5
  import * as path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
6
7
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
8
  import { GROUNDING_ANCHOR_FREE_TEXT, GROUNDING_ANCHOR_ISSUE, GROUNDING_ANCHOR_MIXED, GROUNDING_ANCHOR_RECORD, PROMPT_SOURCE_BUILTIN, PROMPT_SOURCE_OVERRIDE, RUN_ARTIFACT_SCHEMA_VERSION, RunArtifactSchema, } from '@mmnto/totem';
8
9
  import { cleanTmpDir } from '../test-utils.js';
@@ -2106,9 +2107,16 @@ describe('buildPrePushHook with strict tier', () => {
2106
2107
  const hook = buildPrePushHook({ ...RENDER, tier: 'strict' });
2107
2108
  // Find the position of the doctor --strict invocation and the surrounding
2108
2109
  // guard; assert the invocation lives INSIDE the agent-or-tier block.
2109
- const guardIdx = hook.indexOf('if [ "$is_agent" = "1" ] || [ "$TOTEM_HOOK_TIER" = "strict" ]');
2110
2110
  const doctorIdx = hook.indexOf('$TOTEM_CMD doctor --strict');
2111
- const fiCloseIdx = hook.indexOf('fi', guardIdx);
2111
+ // Anchored on the guard that ENCLOSES this invocation: since
2112
+ // mmnto-ai/totem#2698 the pre-push hook carries more than one agent/strict
2113
+ // guard (the review-leg floor arm has its own, and it is rendered first),
2114
+ // so a bare first-match lookup would measure the wrong block.
2115
+ const guardIdx = hook.lastIndexOf('if [ "$is_agent" = "1" ] || [ "$TOTEM_HOOK_TIER" = "strict" ]', doctorIdx);
2116
+ // Anchored to a LINE, not a substring: a bare `fi` also matches the one
2117
+ // inside "findings" in the shield block's comment, which sits between the
2118
+ // invocation and the real closing `fi` (CodeRabbit on PR mmnto-ai/totem#2745).
2119
+ const fiCloseIdx = hook.indexOf(`${String.fromCharCode(10)} fi`, doctorIdx);
2112
2120
  expect(guardIdx).toBeGreaterThan(-1);
2113
2121
  expect(doctorIdx).toBeGreaterThan(guardIdx);
2114
2122
  expect(doctorIdx).toBeLessThan(fiCloseIdx);
@@ -2140,9 +2148,16 @@ describe('buildPrePushHook with standard tier', () => {
2140
2148
  // standard, so the guard branch never enters.
2141
2149
  it('emits doctor --strict gated by agent/strict guard (no unconditional fire in standard tier)', () => {
2142
2150
  const hook = buildPrePushHook(RENDER);
2143
- const guardIdx = hook.indexOf('if [ "$is_agent" = "1" ] || [ "$TOTEM_HOOK_TIER" = "strict" ]');
2144
2151
  const doctorIdx = hook.indexOf('$TOTEM_CMD doctor --strict');
2145
- const fiCloseIdx = hook.indexOf('fi', guardIdx);
2152
+ // Anchored on the guard that ENCLOSES this invocation: since
2153
+ // mmnto-ai/totem#2698 the pre-push hook carries more than one agent/strict
2154
+ // guard (the review-leg floor arm has its own, and it is rendered first),
2155
+ // so a bare first-match lookup would measure the wrong block.
2156
+ const guardIdx = hook.lastIndexOf('if [ "$is_agent" = "1" ] || [ "$TOTEM_HOOK_TIER" = "strict" ]', doctorIdx);
2157
+ // Anchored to a LINE, not a substring: a bare `fi` also matches the one
2158
+ // inside "findings" in the shield block's comment, which sits between the
2159
+ // invocation and the real closing `fi` (CodeRabbit on PR mmnto-ai/totem#2745).
2160
+ const fiCloseIdx = hook.indexOf(`${String.fromCharCode(10)} fi`, doctorIdx);
2146
2161
  expect(guardIdx).toBeGreaterThan(-1);
2147
2162
  expect(doctorIdx).toBeGreaterThan(guardIdx);
2148
2163
  expect(doctorIdx).toBeLessThan(fiCloseIdx);
@@ -2170,4 +2185,444 @@ describe('agent detection uses POSIX syntax', () => {
2170
2185
  expect(hook).toMatch(/^#!\/bin\/sh\n/);
2171
2186
  });
2172
2187
  });
2188
+ // ─── The strict pre-push review-leg floor arm (mmnto-ai/totem#2698) ──────────
2189
+ describe('buildPrePushHook — the review-leg floor arm (mmnto-ai/totem#2698)', () => {
2190
+ const hook = buildPrePushHook(RENDER);
2191
+ it("probes the VERB's own help for an option only that verb has", () => {
2192
+ expect(hook).toContain(`if $TOTEM_CMD legs gate --help 2>/dev/null | grep -q -- '--advisory'; then`);
2193
+ // NOT the group's help for the word `gate` (mmnto-ai/totem#2698 fold 2): a
2194
+ // CLI predating `totem legs` answers any `legs …` with its curated
2195
+ // TOP-LEVEL help and exits 0, so that grep reads a moving surface and the
2196
+ // queued `merge-gate` verb (mmnto-ai/totem#2708) would satisfy it.
2197
+ expect(hook).not.toContain(`$TOTEM_CMD legs --help`);
2198
+ });
2199
+ it('invokes the bare gate on strict and the advisory form otherwise', () => {
2200
+ expect(hook).toContain('$TOTEM_CMD legs gate\n');
2201
+ expect(hook).toContain('$TOTEM_CMD legs gate --advisory');
2202
+ expect(hook).toContain('legs_status=$?');
2203
+ });
2204
+ it('blocks on exit 3 and on any other non-zero with DISTINCT lines', () => {
2205
+ 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)"`);
2206
+ 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)"');
2207
+ expect(hook).toContain('if [ "$legs_status" = "3" ]; then');
2208
+ expect(hook).toContain('elif [ "$legs_status" != "0" ]; then');
2209
+ });
2210
+ it('FAILS CLOSED on strict when the resolved CLI lacks the verb, compat-opens otherwise', () => {
2211
+ // The OPPOSITE of the `--gate` / `--scope-to-diff` precedent, by ruling
2212
+ // (mmnto-ai/totem#2698 OQ2): those flags degrade to a form that still runs;
2213
+ // an absent VERB has no degraded form, so strict blocks with the cure.
2214
+ 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`);
2215
+ expect(hook).toContain(`echo "[totem] Hook running without the legs gate (CLI predates 'totem legs'); 'npm i -g @mmnto/cli@latest' enables it." >&2`);
2216
+ });
2217
+ it('runs BEFORE the shield block and inside the $TOTEM_CMD guard', () => {
2218
+ const cmdGuardIdx = hook.indexOf('if [ -n "$TOTEM_CMD" ]; then');
2219
+ const legsIdx = hook.indexOf('$TOTEM_CMD legs gate --help');
2220
+ const shieldIdx = hook.indexOf('Running shield gate (strict mode)');
2221
+ expect(cmdGuardIdx).toBeGreaterThan(-1);
2222
+ expect(legsIdx).toBeGreaterThan(cmdGuardIdx);
2223
+ // Slotted first so a legs-owed push is not paid for with the slow review gate.
2224
+ expect(legsIdx).toBeLessThan(shieldIdx);
2225
+ });
2226
+ it('gates the blocking arms on the agent/strict guard', () => {
2227
+ const legsIdx = hook.indexOf('$TOTEM_CMD legs gate --help');
2228
+ const guardIdx = hook.indexOf('if [ "$is_agent" = "1" ] || [ "$TOTEM_HOOK_TIER" = "strict" ]', legsIdx);
2229
+ const gateIdx = hook.indexOf('$TOTEM_CMD legs gate\n');
2230
+ expect(guardIdx).toBeGreaterThan(legsIdx);
2231
+ expect(gateIdx).toBeGreaterThan(guardIdx);
2232
+ });
2233
+ });
2234
+ // The arm, EXECUTED. String assertions alone are satisfiable without the
2235
+ // behavior, and the exit-code mapping IS the contract here. The stub `totem` is
2236
+ // the FIRST entry on an isolated PATH and records its argv, so every case
2237
+ // proves the hook actually reached it — an un-isolated PATH or a stub on the
2238
+ // wrong stream is the false-green shape this suite exists to exclude.
2239
+ describe('the review-leg floor arm executed under sh (mmnto-ai/totem#2698)', () => {
2240
+ const shellOk = spawnSync('sh', ['-c', 'exit 0'], { encoding: 'utf-8' }).status === 0;
2241
+ let tmpDir;
2242
+ let repoDir;
2243
+ let binDir;
2244
+ let logPath;
2245
+ beforeEach(() => {
2246
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-legs-hook-'));
2247
+ repoDir = path.join(tmpDir, 'repo');
2248
+ binDir = path.join(tmpDir, 'stub-bin');
2249
+ fs.mkdirSync(repoDir, { recursive: true });
2250
+ fs.mkdirSync(binDir);
2251
+ // Forward slashes: the stub is read by `sh`, and a Windows path's
2252
+ // backslashes would not survive the shell's quoting.
2253
+ logPath = path.join(tmpDir, 'totem-invocations.log').split(path.sep).join('/');
2254
+ });
2255
+ afterEach(() => {
2256
+ cleanTmpDir(tmpDir);
2257
+ });
2258
+ /**
2259
+ * A stub `totem` that records every invocation, answers the `legs --help`
2260
+ * probe with the given help text, and exits `gateExit` for `legs gate`. The
2261
+ * bare repo dir clears every earlier hook gate (no manifest, no compiled
2262
+ * rules, no lockfile, no package.json), so the legs arm and the shield block
2263
+ * are the only live ones.
2264
+ */
2265
+ function writeStub(params) {
2266
+ const stub = [
2267
+ '#!/bin/sh',
2268
+ `printf '%s\\n' "$*" >> "${logPath}"`,
2269
+ // The help arm is tested FIRST and on THREE words: since
2270
+ // mmnto-ai/totem#2698 fold 2 the probe is `legs gate --help`, which also
2271
+ // matches the gate arm's first two.
2272
+ 'if [ "$1" = "legs" ] && [ "$2" = "gate" ] && [ "$3" = "--help" ]; then',
2273
+ " cat <<'TOTEM_STUB_HELP'",
2274
+ params.helpText,
2275
+ 'TOTEM_STUB_HELP',
2276
+ ` exit ${params.helpExit ?? 0}`,
2277
+ 'fi',
2278
+ 'if [ "$1" = "legs" ] && [ "$2" = "gate" ]; then',
2279
+ ' echo "[Totem] legs: stub gate line"',
2280
+ ` exit ${params.gateExit ?? 0}`,
2281
+ 'fi',
2282
+ 'exit 0',
2283
+ '',
2284
+ ].join('\n');
2285
+ fs.writeFileSync(path.join(binDir, 'totem'), stub, { mode: 0o755 });
2286
+ }
2287
+ /**
2288
+ * The verb's OWN help, and the top-level help an older CLI answers with.
2289
+ *
2290
+ * The unsupported fixture is a HOSTILE VARIANT of the 1.122.0 shape, not
2291
+ * that shape verbatim: the measured seven-command top-level help contains no
2292
+ * `gate` at all, so today's probe does not false-pass on it. This fixture
2293
+ * INJECTS a `merge-gate` entry (mmnto-ai/totem#2708 is queued for that list),
2294
+ * so the OLD group-level grep would false-pass and the verb-specific probe
2295
+ * must not. The defect it guards is latent, and this is what keeps it so.
2296
+ */
2297
+ const HELP_WITH_GATE = 'Usage: totem legs gate [options]\n\nOptions:\n --advisory Print the same lines for every state\n -h, --help display help for command';
2298
+ const HELP_WITHOUT_GATE = 'Totem: local-first toolkit\n\nUsage: totem [command]\n\nCommands:\n init Initialize Totem\n lint Run compiled rules\n merge-gate Gate a merge';
2299
+ function runHook(options) {
2300
+ fs.writeFileSync(path.join(repoDir, 'pre-push'), buildPrePushHook({ ...RENDER, tier: options.tier }));
2301
+ // Every agent-detection variable is cleared explicitly: this suite runs
2302
+ // inside an agent session, and an inherited CLAUDE_CODE_AGENT would make
2303
+ // the standard-tier cases silently exercise the strict arm.
2304
+ const env = {
2305
+ ...process.env,
2306
+ PATH: [binDir, process.env['PATH'] ?? ''].join(path.delimiter),
2307
+ };
2308
+ delete env['CLAUDE_CODE_AGENT'];
2309
+ delete env['CLAUDE_VERSION'];
2310
+ delete env['CURSOR_TRACE_ID'];
2311
+ if (options.agent)
2312
+ env['CLAUDE_CODE_AGENT'] = '1';
2313
+ const result = spawnSync('sh', ['./pre-push'], { cwd: repoDir, env, encoding: 'utf-8' });
2314
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
2315
+ }
2316
+ /** Every recorded stub invocation, in order. Empty when it was never reached. */
2317
+ const invocations = () => {
2318
+ const native = logPath.split('/').join(path.sep);
2319
+ return fs.existsSync(native) ? fs.readFileSync(native, 'utf-8').trim().split('\n') : [];
2320
+ };
2321
+ it.skipIf(!shellOk)('probe UNSUPPORTED + strict: blocks with the upgrade line', () => {
2322
+ writeStub({ helpText: HELP_WITHOUT_GATE });
2323
+ const r = runHook({ tier: 'strict', agent: false });
2324
+ expect(invocations()).toContain('legs gate --help');
2325
+ expect(r.status).toBe(1);
2326
+ expect(r.stderr).toContain("this hook expects 'totem legs gate'");
2327
+ expect(r.stderr).toContain('npm i -g @mmnto/cli@latest');
2328
+ // Fail-closed means the gate was never invoked, not that it passed.
2329
+ expect(invocations()).not.toContain('legs gate');
2330
+ });
2331
+ it.skipIf(!shellOk)('probe UNSUPPORTED + standard: passes with the compat line on stderr', () => {
2332
+ writeStub({ helpText: HELP_WITHOUT_GATE });
2333
+ const r = runHook({ tier: 'standard', agent: false });
2334
+ expect(invocations()).toContain('legs gate --help');
2335
+ expect(r.status).toBe(0);
2336
+ expect(r.stderr).toContain("Hook running without the legs gate (CLI predates 'totem legs')");
2337
+ });
2338
+ it.skipIf(!shellOk)('gate exit 3 + strict: blocks with the legs-owed line and the cure', () => {
2339
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 3 });
2340
+ const r = runHook({ tier: 'strict', agent: false });
2341
+ expect(invocations()).toContain('legs gate');
2342
+ expect(r.status).toBe(1);
2343
+ expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed');
2344
+ expect(r.stdout).toContain('totem legs deposit --sha HEAD --from <findings.json>');
2345
+ // The gate's own line is not swallowed.
2346
+ expect(r.stdout).toContain('[Totem] legs: stub gate line');
2347
+ });
2348
+ it.skipIf(!shellOk)('gate exit 2 + strict: blocks with the DISTINCT not-derived line', () => {
2349
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 2 });
2350
+ const r = runHook({ tier: 'strict', agent: false });
2351
+ expect(invocations()).toContain('legs gate');
2352
+ expect(r.status).toBe(1);
2353
+ expect(r.stdout).toContain('the legs gate could not derive (totem legs gate exit status 2)');
2354
+ expect(r.stdout).not.toContain('this push is legs-owed');
2355
+ });
2356
+ it.skipIf(!shellOk)('gate exit 0 + strict: the hook proceeds PAST the arm', () => {
2357
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 0 });
2358
+ const r = runHook({ tier: 'strict', agent: false });
2359
+ expect(invocations()).toContain('legs gate');
2360
+ // The shield block sits after the arm — reaching it proves the arm passed.
2361
+ expect(invocations()).toContain('doctor --strict');
2362
+ expect(r.status).toBe(0);
2363
+ expect(r.stdout).not.toContain('BLOCKED');
2364
+ });
2365
+ it.skipIf(!shellOk)('an AGENT on the standard tier still takes the strict arm', () => {
2366
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 3 });
2367
+ const r = runHook({ tier: 'standard', agent: true });
2368
+ expect(invocations()).toContain('legs gate');
2369
+ expect(invocations()).not.toContain('legs gate --advisory');
2370
+ expect(r.status).toBe(1);
2371
+ });
2372
+ it.skipIf(!shellOk)('standard tier, no agent: --advisory is passed and exit 3 does NOT block', () => {
2373
+ writeStub({ helpText: HELP_WITH_GATE, gateExit: 3 });
2374
+ const r = runHook({ tier: 'standard', agent: false });
2375
+ // Read from the recorded argv, not from the hook text: the flag has to
2376
+ // have reached the CLI, not merely appear in the template.
2377
+ expect(invocations()).toContain('legs gate --advisory');
2378
+ expect(invocations()).not.toContain('legs gate');
2379
+ expect(r.status).toBe(0);
2380
+ expect(r.stdout).not.toContain('BLOCKED');
2381
+ });
2382
+ });
2383
+ // The arm COMPOSED with the real gate (mmnto-ai/totem#2698 fold 2, MATERIAL).
2384
+ //
2385
+ // The stub suite above proves the hook's exit MAPPING — it scripts the gate's
2386
+ // status, so the gate's own derivation is never exercised through the hook, and
2387
+ // a gate that stopped returning 3 for an owed-and-unanswered push would leave
2388
+ // every one of those tests green. This suite closes that: the shim on the
2389
+ // isolated PATH DELEGATES `legs` to the real built CLI, so each case runs the
2390
+ // whole composition — predicate, store, ancestry, exit code, hook arm, hook
2391
+ // line. Everything after the arm (the shield block's `doctor --strict` and
2392
+ // `review --gate`) is a different gate with its own coverage and stays stubbed,
2393
+ // so this suite's verdict never depends on state it does not control.
2394
+ //
2395
+ // COVERAGE DECLARATION (Q5). It needs `packages/cli/dist/index.js`, which
2396
+ // exists after this repo's CI builds before it tests (`pnpm -r build` precedes
2397
+ // the test run); when it does not, every case SKIPS rather than passing
2398
+ // vacuously, and the skip is visible in the reporter. It runs wherever `sh`
2399
+ // resolves: win32 through git-bash, plus the ubuntu and macos CI legs. The stub
2400
+ // suite above is retained and carries the mapping, including the states this
2401
+ // suite cannot reach through a real CLI (a build that predates the verb).
2402
+ //
2403
+ // One case inside it is POSIX-only, and says so at its own site: a quote- and
2404
+ // backslash-bearing owed path cannot exist on NTFS. Every other case, the
2405
+ // non-ASCII one included, runs on win32 too.
2406
+ describe('the review-leg floor arm COMPOSED with the real gate (mmnto-ai/totem#2698)', () => {
2407
+ /** Built, never authored as an escape (the banked decode trap). */
2408
+ const NL = String.fromCharCode(10);
2409
+ /**
2410
+ * A NON-ASCII owed path (mmnto-ai/totem#2698 fold 4). git C-quotes this name
2411
+ * on BOTH surfaces the gate reads, in two different ways, so an un-normalized
2412
+ * pair reports a covering leg as covering nothing. Driven here through the
2413
+ * REAL CLI, where the argv and the quoting actually happen.
2414
+ */
2415
+ const ACCENTED = `docs/caf${String.fromCharCode(0xe9)}.md`;
2416
+ /** The two-character sequence `printf` needs in the shim, likewise built. */
2417
+ const BACKSLASH_N = String.fromCharCode(92) + 'n';
2418
+ const DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../dist/index.js');
2419
+ const shellOk = spawnSync('sh', ['-c', 'exit 0'], { encoding: 'utf-8' }).status === 0;
2420
+ const composedOk = shellOk && fs.existsSync(DIST);
2421
+ /**
2422
+ * The REAL working directory, restored after each case: `cross-spawn` (which
2423
+ * the CLI's `safeExec` goes through) chdirs into a child's cwd and restores
2424
+ * with `process.cwd()`, which on Windows would otherwise leave this process
2425
+ * holding the temp repo open and fail teardown.
2426
+ */
2427
+ const realCwd = process.cwd();
2428
+ let tmpDir;
2429
+ let repoDir;
2430
+ let binDir;
2431
+ let logPath;
2432
+ let baseSha;
2433
+ let ancestorSha;
2434
+ let headSha;
2435
+ function git(...args) {
2436
+ return execFileSync('git', args, { cwd: repoDir, encoding: 'utf-8' });
2437
+ }
2438
+ beforeEach(() => {
2439
+ if (!composedOk)
2440
+ return;
2441
+ tmpDir = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'totem-legs-composed-')));
2442
+ repoDir = path.join(tmpDir, 'repo');
2443
+ binDir = path.join(tmpDir, 'stub-bin');
2444
+ fs.mkdirSync(repoDir, { recursive: true });
2445
+ fs.mkdirSync(binDir);
2446
+ logPath = path.join(tmpDir, 'totem-invocations.log').split(path.sep).join('/');
2447
+ git('init', '-q', '-b', 'main');
2448
+ git('config', 'user.email', 'leg@example.test');
2449
+ git('config', 'user.name', 'Leg');
2450
+ // The minimal config the schema accepts, declaring ONE glob so the
2451
+ // owed / not-owed axis of this suite is unambiguous.
2452
+ fs.writeFileSync(path.join(repoDir, 'totem.config.ts'), [
2453
+ 'export default {',
2454
+ " targets: [{ glob: 'docs/**/*.md', type: 'spec', strategy: 'markdown-heading' }],",
2455
+ " hooks: { legsOwed: { globs: ['docs/**'] } },",
2456
+ '};',
2457
+ '',
2458
+ ].join(NL));
2459
+ fs.writeFileSync(path.join(repoDir, 'src.ts'), 'export const a = 1;' + NL);
2460
+ git('add', '.');
2461
+ git('commit', '-q', '-m', 'base');
2462
+ baseSha = git('rev-parse', 'HEAD').trim();
2463
+ // The branch under test: it touches the declared glob, so it is legs-owed.
2464
+ git('checkout', '-q', '-b', 'feat/owed');
2465
+ fs.mkdirSync(path.join(repoDir, 'docs'), { recursive: true });
2466
+ fs.writeFileSync(path.join(repoDir, 'docs', 'note.md'), '# a judgment-dense page' + NL);
2467
+ fs.writeFileSync(path.join(repoDir, ...ACCENTED.split('/')), '# an accented page' + NL);
2468
+ git('add', '-A', 'docs');
2469
+ git('commit', '-q', '-m', 'a docs change');
2470
+ // C1 — the head a leg could read, and an ANCESTOR of the head being pushed.
2471
+ ancestorSha = git('rev-parse', 'HEAD').trim();
2472
+ // C2 — one commit further, touching nothing owed, so the owed set is still
2473
+ // C1's two docs pages and a deposit at C1 covers all of them. Without it
2474
+ // that deposit would be EXACT, credited by construction, and the reach
2475
+ // probe this suite exists to exercise would never run.
2476
+ fs.writeFileSync(path.join(repoDir, 'src.ts'), 'export const a = 2;' + NL);
2477
+ git('add', 'src.ts');
2478
+ git('commit', '-q', '-m', 'a code-only change');
2479
+ headSha = git('rev-parse', 'HEAD').trim();
2480
+ // The shim IS the real CLI for `legs`, recorded either way. First on PATH,
2481
+ // so the hook's resolve block finds it as a plain `totem` (the temp repo has
2482
+ // no package.json, no node_modules and no workspace file, so no earlier
2483
+ // resolution tier can win).
2484
+ fs.writeFileSync(path.join(binDir, 'totem'), [
2485
+ '#!/bin/sh',
2486
+ `printf '%s${BACKSLASH_N}' "$*" >> "${logPath}"`,
2487
+ 'if [ "$1" = "legs" ]; then',
2488
+ ` exec node "${DIST.split(path.sep).join('/')}" "$@"`,
2489
+ 'fi',
2490
+ 'exit 0',
2491
+ '',
2492
+ ].join(NL), { mode: 0o755 });
2493
+ });
2494
+ afterEach(() => {
2495
+ if (!composedOk)
2496
+ return;
2497
+ process.chdir(realCwd);
2498
+ cleanTmpDir(tmpDir);
2499
+ });
2500
+ function runHook() {
2501
+ fs.writeFileSync(path.join(repoDir, 'pre-push'), buildPrePushHook({ ...RENDER, tier: 'strict' }));
2502
+ const env = {
2503
+ ...process.env,
2504
+ PATH: [binDir, process.env['PATH'] ?? ''].join(path.delimiter),
2505
+ };
2506
+ delete env['CLAUDE_CODE_AGENT'];
2507
+ delete env['CLAUDE_VERSION'];
2508
+ delete env['CURSOR_TRACE_ID'];
2509
+ const result = spawnSync('sh', ['./pre-push'], { cwd: repoDir, env, encoding: 'utf-8' });
2510
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
2511
+ }
2512
+ const invocations = () => {
2513
+ const native = logPath.split('/').join(path.sep);
2514
+ return fs.existsSync(native) ? fs.readFileSync(native, 'utf-8').trim().split(NL) : [];
2515
+ };
2516
+ /** Deposit through the REAL writer, at `sha`. */
2517
+ function deposit(sha) {
2518
+ fs.writeFileSync(path.join(repoDir, 'findings.json'), JSON.stringify({
2519
+ readAt: new Date().toISOString(),
2520
+ findings: [
2521
+ {
2522
+ id: 'f1',
2523
+ severity: 'MATERIAL',
2524
+ file: 'docs/note.md',
2525
+ line: 1,
2526
+ claim: 'the page overstates the floor',
2527
+ counterexample: 'the gate never reads severities',
2528
+ },
2529
+ ],
2530
+ folded: ['f1'],
2531
+ verdict: 'one material finding, folded',
2532
+ }));
2533
+ const result = spawnSync('node', [DIST, 'legs', 'deposit', '--sha', sha, '--from', 'findings.json'], { cwd: repoDir, encoding: 'utf-8' });
2534
+ // Anchored, with the writer's own stderr as the failure message: the
2535
+ // previous `toContain('0:')` also passed on a status of 10 or 130.
2536
+ expect(result.status, result.stderr ?? '').toBe(0);
2537
+ }
2538
+ it.skipIf(!composedOk)('OWED with no deposit: the hook blocks, and BOTH lines are present', () => {
2539
+ const r = runHook();
2540
+ expect(invocations()).toContain('legs gate');
2541
+ expect(r.status).toBe(1);
2542
+ // The GATE's own line, derived end to end from the real predicate + store.
2543
+ // Both owed paths are named, the accented one spelled as it is on disk.
2544
+ expect(r.stdout).toContain(`[Totem] BLOCKED: this push is legs-owed (docs/** → ${ACCENTED}, docs/** → docs/note.md)`);
2545
+ expect(r.stdout).toContain(`for head ${headSha.slice(0, 8)}`);
2546
+ // And the HOOK's own cure line, which only the strict arm emits.
2547
+ expect(r.stdout).toContain("run the leg, then 'totem legs deposit --sha HEAD --from <findings.json>'");
2548
+ });
2549
+ it.skipIf(!composedOk)('OWED with an ANCESTOR deposit that covers the owed set: the hook passes', () => {
2550
+ deposit(ancestorSha);
2551
+ const r = runHook();
2552
+ expect(invocations()).toContain('legs gate');
2553
+ expect(r.stdout).toContain('[Totem] legs evidence: .totem/artifacts/legs/');
2554
+ // ANCESTOR, so the reach probe actually RAN: this is the only composed
2555
+ // state that drives `git diff --name-only -z base...<deposit>` end to
2556
+ // end. Depositing at the head took core's exact branch, which credits
2557
+ // coverage by construction and never probes (mmnto-ai/totem#2698 fold 5).
2558
+ expect(r.stdout).toContain(`· head ${headSha.slice(0, 8)} · nearest ancestor, +1 commits since the leg read ·`);
2559
+ // 2/2 with an accented owed path among them: the measured result of
2560
+ // reading both sides raw.
2561
+ expect(r.stdout).toContain('· covers 2/2 owed paths ·');
2562
+ expect(r.stdout).toContain('blocking=0 material=1 folded=1');
2563
+ expect(r.stdout).not.toContain('BLOCKED');
2564
+ expect(r.status).toBe(0);
2565
+ });
2566
+ it.skipIf(!composedOk)('OWED with a deposit on a SIBLING branch: the hook blocks and the gate calls it stale', () => {
2567
+ // A commit that is not an ancestor of HEAD: a second branch off the base.
2568
+ git('checkout', '-q', '-b', 'feat/sibling', baseSha);
2569
+ fs.writeFileSync(path.join(repoDir, 'other.ts'), 'export const b = 2;' + NL);
2570
+ git('add', 'other.ts');
2571
+ git('commit', '-q', '-m', 'a sibling commit');
2572
+ const siblingSha = git('rev-parse', 'HEAD').trim();
2573
+ git('checkout', '-q', 'feat/owed');
2574
+ deposit(siblingSha);
2575
+ const r = runHook();
2576
+ expect(invocations()).toContain('legs gate');
2577
+ expect(r.status).toBe(1);
2578
+ expect(r.stdout).toContain(`[Totem] legs: stale deposit ${siblingSha.slice(0, 8)}: not an ancestor of head`);
2579
+ expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed');
2580
+ });
2581
+ it.skipIf(!composedOk)('OWED with a MERGE-BASE deposit: ancestor, but it covers none of the owed paths', () => {
2582
+ // The fold-3 exhibit, end to end: a deposit at the base is a real
2583
+ // ancestor one commit behind, so ancestry alone passed it — while the leg
2584
+ // that wrote it saw none of the diff this push proposes.
2585
+ deposit(baseSha);
2586
+ const r = runHook();
2587
+ expect(invocations()).toContain('legs gate');
2588
+ expect(r.status).toBe(1);
2589
+ expect(r.stdout).toContain(`[Totem] legs: stale deposit ${baseSha.slice(0, 8)}: covers none of the owed paths (the deposit predates every owed change)`);
2590
+ expect(r.stdout).toContain('[Totem] BLOCKED: this push is legs-owed');
2591
+ // And it is NOT reported as an ancestry failure — the reason is the cure.
2592
+ expect(r.stdout).not.toContain('not an ancestor of head');
2593
+ });
2594
+ // POSIX-only, and only for these two names: NTFS refuses a double quote and a
2595
+ // backslash in a filename, while Linux and macOS — where the strict-tier
2596
+ // population runs — allow both, and git C-quotes both in the `diff --git`
2597
+ // headers the owed set used to be parsed from. This drives them through the
2598
+ // REAL CLI and the generated hook (mmnto-ai/totem#2698 fold 5).
2599
+ it.skipIf(!composedOk || process.platform === 'win32')('OWED on quote- and backslash-bearing paths: the hook names them as they are on disk', () => {
2600
+ const quoted = `docs/a${String.fromCharCode(34)}quoted${String.fromCharCode(34)}.md`;
2601
+ const backslashed = `docs/back${String.fromCharCode(92)}slash.md`;
2602
+ fs.writeFileSync(path.join(repoDir, ...quoted.split('/')), '# quoted' + NL);
2603
+ fs.writeFileSync(path.join(repoDir, ...backslashed.split('/')), '# backslashed' + NL);
2604
+ git('add', '-A', 'docs');
2605
+ git('commit', '-q', '-m', 'the hostile pages');
2606
+ const r = runHook();
2607
+ expect(invocations()).toContain('legs gate');
2608
+ expect(r.status).toBe(1);
2609
+ // Raw in the basis. Escaped names here would be the fold-4 defect, and
2610
+ // the decoder that fold shipped could not have reached either of these.
2611
+ expect(r.stdout).toContain(quoted);
2612
+ expect(r.stdout).toContain(backslashed);
2613
+ expect(r.stdout).not.toContain(String.fromCharCode(92) + '"');
2614
+ });
2615
+ it.skipIf(!composedOk)('NOT OWED: the hook passes and the gate says what it judged', () => {
2616
+ // A branch whose only change misses the one declared glob.
2617
+ git('checkout', '-q', '-b', 'feat/not-owed', baseSha);
2618
+ fs.writeFileSync(path.join(repoDir, 'src.ts'), 'export const a = 2;' + NL);
2619
+ git('add', 'src.ts');
2620
+ git('commit', '-q', '-m', 'a code-only change');
2621
+ const r = runHook();
2622
+ expect(invocations()).toContain('legs gate');
2623
+ expect(r.status).toBe(0);
2624
+ expect(r.stdout).toContain('[Totem] legs: not owed — no changed path matched hooks.legsOwed.globs (1 globs; head');
2625
+ expect(r.stdout).not.toContain('BLOCKED');
2626
+ });
2627
+ });
2173
2628
  //# sourceMappingURL=install-hooks.test.js.map