
    +Vjg                       d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlmZmZ ddlmZ ddlmZmZmZmZmZ ddlmZ  ej        e          ZdZd	d
hZ ej        d          Zd;dZd;dZd;dZ dZ!d<dZ"d=d>dZ#d?dZ$d@dZ%dAdZ&dBdZ'	 d=dCd&Z(dDdEd)Z)dFd,Z*dGd.Z+dHd0Z,dId3Z-dJd5Z.d=dKd7Z/dLd9Z0dMd:Z1dS )NuK  Curator snapshot + rollback.

A pre-run snapshot of ``~/.hermes/skills/`` (excluding ``.curator_backups/``
itself) is taken before any mutating curator pass. Snapshots are tar.gz
files under ``~/.hermes/skills/.curator_backups/<utc-iso>/`` with a
companion ``manifest.json`` describing the snapshot (reason, time, size,
counted skill files). Rollback picks a snapshot, moves the current
``skills/`` tree aside into another snapshot so even the rollback itself
is undoable, then extracts the chosen snapshot into place.

The snapshot does NOT include:
  - ``.curator_backups/`` (would recurse)
  - ``.hub/`` (hub-installed skills — managed by the hub, not us)

It DOES include:
  - all SKILL.md files + their directories (``scripts/``, ``references/``,
    ``templates/``, ``assets/``)
  - ``.usage.json`` (usage telemetry — needed to rehydrate state cleanly)
  - ``.archive/`` (so rollback restores previously-archived skills too)
  - ``.curator_state`` (so rolling back also restores the last-run-at
    pointer — otherwise the curator would immediately re-fire on the next
    tick)
  - ``.bundled_manifest`` (so protection markers stay consistent)

Alongside the skills tarball, each snapshot also captures a copy of
``~/.hermes/cron/jobs.json`` as ``cron-jobs.json`` when it exists. Cron
jobs reference skills by name in their ``skills``/``skill`` fields; the
curator's consolidation pass rewrites those in place via
``cron.jobs.rewrite_skill_refs()``. Without capturing the pre-run state,
rolling back the skills tree would leave cron jobs pointing at the
umbrella skills even though the narrow skills they were originally
configured with have been restored. We store the whole jobs.json for
fidelity but rollback only touches the ``skills``/``skill`` fields — the
rest (schedule, next_run_at, enabled, prompt, etc.) is live state and
we leave it alone.
    )annotationsN)datetimetimezone)Path)AnyDictListOptionalTupleget_hermes_home   .curator_backupsz.hubz/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(-\d{2})?$returnr   c                 *    t                      dz  dz  S )Nskillsr   r        2/root/.hermes/hermes-agent/agent/curator_backup.py_backups_dirr   F   s    x'*<<<r   c                 $    t                      dz  S )Nr   r   r   r   r   _skills_dirr   J   s    x''r   c                 *    t                      dz  dz  S )zHSource path for the live cron jobs store (``~/.hermes/cron/jobs.json``).cronz	jobs.jsonr   r   r   r   _cron_jobs_filer   N   s    v%33r   zcron-jobs.jsondestDict[str, Any]c                8   t                      }ddd}|                                sd|d<   |S 	 |                    d          }n<# t          $ r/}t                              d|           d	| |d<   |cY d
}~S d
}~ww xY w	 t          j        |          }t          |t                    r=|
                    d          }t          |t                    rt          |          |d<   n't          |t                    rt          |          |d<   n&# t          j        t          f$ r d|d<   d|d<   Y nw xY w	 | t          z                      |d           n<# t          $ r/}t                              d|           d| |d<   |cY d
}~S d
}~ww xY wd|d<   |S )u  Copy the live cron jobs.json into ``dest`` as ``cron-jobs.json``.

    Returns a small dict describing what was captured so the caller can
    fold it into the manifest. Never raises — if the cron file is missing
    or unreadable, the return dict has ``backed_up=False`` and the reason,
    and the snapshot proceeds without cron data (the snapshot is still
    useful for rolling back skills).
    Fr   	backed_up
jobs_countzno cron/jobs.json presentreasonutf-8encodingz,Failed to read cron/jobs.json for backup: %szread error: Njobsr!   z-jobs.json was not valid JSON at snapshot timeparse_warningz$Failed to write cron backup file: %szwrite error: Tr    )r   exists	read_textOSErrorloggerdebugjsonloads
isinstancedictgetlistlenJSONDecodeError	TypeErrorCRON_JOBS_FILENAME
write_text)r   srcinforaweparsedinners          r   _backup_cron_jobs_intor>   V   s    

C).a@@D::<< 4XmmWm--   CQGGG+++X
PCfd## 	-JJv&&E%&& 0%(ZZ\"%% 	-!$VD ), P P P\ O_P	"	"..sW.EEEE   ;Q???,,,X DKsN   A 
B $A;5B ;B BD  D54D59E 
F#$FFFnowOptional[datetime]strc                   | t          j        t          j                  } |                     d                                          }|                    d          r
|dd         }|                    dd          dz   S )	z@UTC ISO-ish filesystem-safe timestamp: ``2026-05-01T13-05-42Z``.Nr   )microsecondz+00:00i:-Z)r   r?   r   utcreplace	isoformatendswith)r?   ss     r   _utc_idrL      sq    
{l8<(("",,..Azz( crcF99S#$$r   c                 x   	 ddl m}   |             }n4# t          $ r'}t                              d|           i cY d }~S d }~ww xY wt          |t                    si S |                    d          pi }t          |t                    si S |                    d          pi }t          |t                    r|ni S )Nr   )load_configz,Failed to load config for curator backup: %scuratorbackup)hermes_cli.configrN   	Exceptionr+   r,   r/   r0   r1   )rN   cfgr;   curbks        r   _load_configrV      s    111111kmm   CQGGG						 c4   	
'')


"Cc4   					 bBB%%-222-s    
A?AAboolc                 `    t          t                                          dd                    S )uB   Default ON — the whole point of the backup is safety by default.enabledT)rW   rV   r1   r   r   r   
is_enabledrZ      s$    ""9d33444r   intc                     t                      } 	 t          |                     dt                              }n# t          t
          f$ r
 t          }Y nw xY wt          d|          S )Nkeep   )rV   r[   r1   DEFAULT_KEEPr5   
ValueErrormax)rS   ns     r   get_keeprc      sd    
..C--..z"   q!99s   (9 AAbasec                ~    	 t          d |                     d          D                       S # t          $ r Y dS w xY w)Nc              3     K   | ]}d V  dS )r^   Nr   ).0_s     r   	<genexpr>z%_count_skill_files.<locals>.<genexpr>   s"      551555555r   zSKILL.mdr   )sumrglobr*   )rd   s    r   _count_skill_filesrl      sR    55djj44555555   qqs   +. 
<<r"   archive_pathskills_counted	cron_infoOptional[Dict[str, Any]]Nonec                `   | j         |t          j        t          j                                                  |j         |                                j        |d}|t          |	                    dd                    t          |	                    dd                    d|d<   |	                    d          s|	                    dd	          |d         d<   |	                    d
          r|d
         |d         d
<   | dz                      t          j        |dd          d           d S )N)idr"   
created_atarchivearchive_bytesskill_filesr    Fr!   r   r   	cron_jobsr"   znot capturedr'   manifest.json   T)indent	sort_keysr#   r$   )namer   r?   r   rG   rI   statst_sizerW   r1   r[   r7   r-   dumps)r   r"   rm   rn   ro   manifests         r   _write_manifestr      s8    il8<00::<<$%**,,4% H immK??@@immL!<<==!
 !
 }}[)) 	V.7mmHn.U.UH[!(+==)) 	P5>5OH[!/2	O''
8A666 (     r   manualOptional[Path]c                   t                      st                              d           dS t                      }|                                st                              d           dS t                      }	 |                    dd           n4# t          $ r'}t                              d||           Y d}~dS d}~ww xY wt                      }|}d}||z                                  r$| d|d	}|dz  }||z                                  $||z  }	 |                    dd
           n4# t          $ r'}t                              d||           Y d}~dS d}~ww xY w|dz  }	 t          j
        |dd          5 }	t          |                                          D ];}
|
j        t          v r|	                    t!          |
          |
j        d           <	 ddd           n# 1 swxY w Y   t#          |          }t%          || |t'          |          |           ni# t          t          j        f$ rP}t                              d|d           	 t+          j        |d           n# t          $ r Y nw xY wY d}~dS d}~ww xY wt/          t1                                 t                              d||            |S )uY  Create a tar.gz snapshot of ``~/.hermes/skills/`` and prune old ones.

    Returns the snapshot directory path, or ``None`` if the snapshot was
    skipped (backup disabled, skills dir missing, or an IO error occurred —
    in which case we log at debug and return None so the curator never
    aborts a pass because of a backup failure).
    z4Curator backup disabled by config; skipping snapshotNu5   No ~/.hermes/skills/ directory — nothing to back upTparentsexist_okz#Failed to create backups dir %s: %sr^   rE   02dFz$Failed to create snapshot dir %s: %sskills.tar.gzzw:gz   )compresslevel)arcname	recursive)ro   zCurator snapshot failed: %sexc_infoignore_errors)r]   z!Curator snapshot created: %s (%s))rZ   r+   r,   r   r(   r   mkdirr*   rL   tarfileopensortediterdirr}   _EXCLUDE_TOP_LEVELaddrA   r>   r   rl   TarErrorshutilrmtree
_prune_oldrc   r9   )r"   r   backupsr;   base_idsnap_idcounterr   ru   tfentryro   s               r   snapshot_skillsr      s    << KLLLt]]F==?? LMMMtnnGdT2222   :GQGGGttttt iiGGGW
$
$
&
& ,,w,,,1 W
$
$
&
&  WD

4%
0000   ;T1EEEttttt _$G\'6;;; 	Gr 0 011 G G:!333 s5zz5:FFFFG	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G 	G +400	fg*622"+	- 	- 	- 	- 	- W%&   2AEEE	M$d33333 	 	 	D	ttttt HJJ
KK3WfEEEKs   8B 
CB<<CD/ /
E 9EE )H'  AG+H' +G//H' 2G/33H' 'J=JI21J2
I?<J>I??JJr]   	List[str]c                6   t                      }|                                sg S g }g }|                                D ]}|                                s|j                            d          r|                    |           Gt                              |j                  r|                    |j        |f           |	                    d d           g }|| d         D ]g\  }}	 t          j        |           |                    |j                   5# t          $ r&}t                              d||           Y d}~`d}~ww xY w|D ]J}	 t          j        |           # t          $ r&}t                              d||           Y d}~Cd}~ww xY w|S )zDelete regular snapshots beyond the newest *keep*. Returns deleted
    ids. Staging dirs (``.rollback-staging-*``) are implementation detail
    and pruned independently on every call..rollback-staging-c                    | d         S )Nr   r   )ts    r   <lambda>z_prune_old.<locals>.<lambda>/  s
    qt r   T)keyreverseNzFailed to prune %s: %sz(Failed to clean stale staging dir %s: %s)r   r(   r   is_dirr}   
startswithappend_ID_REmatchsortr   r   r*   r+   r,   )	r]   r   entriesstale_stagingchilddeletedrh   pathr;   s	            r   r   r     s    nnG>> 	&(G "M"" 
0 
0||~~ 	:  !566 	   '''<<
## 	0NNEJ.///LL^^TL222G455> < <4	<M$NN49%%%% 	< 	< 	<LL14;;;;;;;;	< N N	NM$ 	N 	N 	NLLCT1MMMMMMMM	NNs0   *.D
E	#EE	E&&
F0FFsnap_dirc                    | dz  }|                                 si S 	 t          j        |                    d                    S # t          t          j        f$ r i cY S w xY w)Nry   r#   r$   )r(   r-   r.   r)   r*   r4   )r   mfs     r   _read_manifestr   C  sp    	O	#B99;; 	z",,,88999T)*   			s   'A A A List[Dict[str, Any]]c                 t   t                      } |                                 sg S g }t          |                                 d          D ]}|                                st
                              |j                  s7|dz                                  sOt          |          }|	                    d|j                   |	                    dt          |                     d|vr8|dz  }	 |                                j        |d<   n# t          $ r d|d<   Y nw xY w|                    |           |S )u   Return all restorable snapshots, newest first. Only entries with a
    real ``skills.tar.gz`` tarball are listed — transient
    ``.rollback-staging-*`` directories created mid-rollback are
    implementation detail and not shown.Tr   r   rs   r   rv   r   )r   r(   r   r   r   r   r   r}   r   
setdefaultrA   r~   r   r*   r   )r   outr   r   arcs        r   list_backupsr   M  sG   
 nnG>> 	 "C))4888  ||~~ 	||EJ'' 	'//11 	E""
dEJ'''
fc%jj)))"$$/)C(&)hhjj&8?## ( ( (&'?###(

2Js   0DDD	backup_idOptional[str]c                \   t                      }|                                sdS | rN|| z  }|                                r3t                              |           r|dz                                  r|S dS d t          |                                d          D             }|r|d         ndS )zpReturn the path of the requested backup, or the newest one if
    *backup_id* is None. Returns None if no match.Nr   c                    g | ]N}|                                 t                              |j                  5|d z                                  L|OS )r   )r   r   r   r}   r(   )rg   cs     r   
<listcomp>z#_resolve_backup.<locals>.<listcomp>y  se       88:: ,,qv..454G3O3O3Q3Q	  r   Tr   r   )r   r(   r   r   r   r   r   )r   r   target
candidatess       r   _resolve_backupr   j  s     nnG>> t 9$MMOO	Y''	 /)1133	
 Mt '//++T:::  J '0:a==D0r   snapshot_dirc           
        dg g ddd}| t           z  }|                                sdt            |d<   |S 	 |                    d          }t          j        |          }n-# t
          t          j        f$ r}d	| |d<   |cY d}~S d}~ww xY wt          |t                    r|	                    d
          }nt          |t                    r|}nd}t          |t                    sd|d<   |S i }|D ]}t          |t                    s|	                    d          }	t          |	t                    r|	sE|	                    d          |	                    d          |	                    d          p|	d||	<   |sd|d<   |S 	 ddlm}
m}m} n!# t           $ r}d| |d<   |cY d}~S d}~ww xY wd|d<   	 |5   |
            }d}t#                      }|D ]]}t          |t                    s|	                    d          }	t          |	t                    r|	sF|                    |	           |	                    |	          }|s|	                    d          }|	                    d          }|	                    d          }|	                    d          }||k    r||k    r|dxx         dz  cc<   ||                    dd           n||d<   ||                    dd           n||d<   |d                             |	|	                    d          p|	||d||dd           d}_|                                D ]<\  }	}|	|vr3|d                             |	|	                    d          p|	d           =|r ||           ddd           n# 1 swxY w Y   n<# t,          $ r/}t.                              d|d           d| |d<   Y d}~nd}~ww xY w|S )u]  Reconcile backed-up cron skill links into the live ``cron/jobs.json``.

    We do NOT overwrite the whole cron file. Only the ``skills`` and
    ``skill`` fields are restored, and only on jobs that still exist in the
    current file (matched by ``id``). Everything else about the job —
    schedule, next_run_at, last_run_at, enabled, prompt, workdir, hooks —
    is live state that the user/scheduler has modified since the snapshot;
    overwriting it would regress unrelated cron activity.

    Rules:
    - Jobs present in backup AND live, with differing skills → skills restored.
    - Jobs present in backup AND live, with matching skills → no-op.
    - Jobs present in backup but gone from live (user deleted the job
      after the snapshot) → skipped, noted in the return report.
    - Jobs present in live but not in backup (user created a new cron
      job after the snapshot) → left untouched.

    Never raises; failures are captured in the return dict. Writes through
    ``cron.jobs`` to pick up the same lock + atomic-write path that tick()
    uses, so we don't race the scheduler.
    Fr   N)	attemptedrestoredskipped_missing	unchangederrorzsnapshot has no r   r#   r$   zfailed to load backed-up jobs: r&   z)backed-up cron-jobs.json has no jobs listrs   r   skillr}   )r   r   r}   Tr   )	load_jobs	save_jobs_jobs_file_lockzcron module unavailable: r   r^   r   )r   r   )job_idjob_namefromtor   )r   r   z"Cron skill-link restore failed: %sr   zrestore failed mid-flight: )r6   r(   r)   r-   r.   r*   r4   r/   r0   r1   r2   rA   	cron.jobsr   r   r   ImportErrorsetr   popr   itemsrR   r+   r,   )r   reportbackup_filebackup_textbackup_parsedr;   backup_jobsbackup_by_idjobjidr   r   r   	live_jobschangedlive_idsliverP   
cur_skills	cur_skill
bkp_skills	bkp_skills                         r   _restore_cron_skill_linksr     sR   .  F !33K A-?AAw!++W+==
;//T)*   ?A??w
 -&& #''//	M4	(	( #k4(( Ew /1L 

 

#t$$ 	ggdmm#s## 	3 	ggh''WWW%%GGFOO*s
 
S  "{CCCCCCCCCCC   9a99w F;:< 6	% 6	%!	IGuuH! & &!$-- hhtnn!#s++ 3 S!!!%))#..>!XXh//
 HHW--	#ZZ11
"JJw//	++	Y0F0F;'''1,''' %HHXt,,,,%/DN$HHWd++++$-DMz"))! &

6 2 2 9c'1IFF%/)DD	+ +      ,1133  Vh&&,-44"%$*JJv$6$6$=#6 6   
  %	)$$$m6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	% 6	%n  < < <91tLLL;;;w< Msx   *A" "B8	BBB
F 
F1	F,&F1,F1:N. <GN"N. "N&&N. )N&*N. .
O'8%O""O' Tuple[bool, str, Optional[Path]]c                
   t          |           }|dd| rd|  dndz   dz   dfS |dz  }|                                sdd	|j         d
dfS t                      }|                    dd           t                      }|                    dd           	 t          d|j                    n# t          $ r}dd| dfcY d}~S d}~ww xY w|dt                       z  }	 |                    dd           n# t          $ r}dd| dfcY d}~S d}~ww xY wg }	 t          |                                          D ]a}|j        t          v r||j        z  }	t          j        t          |          t          |	                     |                    ||	f           bn# t          $ r}|D ]E\  }
}		 t          j        t          |	          t          |
                     6# t          $ r Y Bw xY w	 t          j        |d           n# t          $ r Y nw xY wdd| dfcY d}~S d}~ww xY w	 t%          j        |d          5 }|                                D ]K}|j        }|                    d          sdt-          |          j        v rt%          j        d|          L	 |                    t          |          d           n2# t4          $ r% |                    t          |                     Y nw xY wddd           n# 1 swxY w Y   n# t          t$          j        f$ r}|D ]E\  }
}		 t          j        t          |	          t          |
                     6# t          $ r Y Bw xY w	 t          j        |d           n# t          $ r Y nw xY wdd| dfcY d}~S d}~ww xY w	 t          j        |d           n# t          $ r Y nw xY wt7          |          }d|j         g}|                    d          r7t;          |                    d          pg           }t;          |                    d          pg           }|                    d          r|                    d |d                     n|d!k    r!|d!k    r|                    d"d!          d!k    rng }|r|                    | d#           |r|                    | d$           |                    d"          r|                    |d"          d%           |                    d&d'                    |          z              t>                               d(|j        |           dd)                    |          |fS )*ao  Restore ``~/.hermes/skills/`` from a snapshot.

    Strategy:
      1. Resolve the target snapshot (explicit id or newest regular).
      2. Take a safety snapshot of the CURRENT skills tree under
         ``.curator_backups/pre-rollback-<ts>/`` so the rollback itself is
         undoable.
      3. Move all current top-level entries (except ``.curator_backups``
         and ``.hub``) into a tempdir.
      4. Extract the chosen snapshot into ``~/.hermes/skills/``.
      5. On failure during 4, move the tempdir contents back (best-effort)
         and return failure.

    Returns ``(ok, message, snapshot_path)``.
    NFzno matching backup foundz	 for id '' zB (use `hermes curator rollback --list` to see available snapshots)r   z	snapshot u$    has no skills.tar.gz — corrupted?Tr   zpre-rollback to )r"   z%pre-rollback safety snapshot failed: r   zfailed to create staging dir: r   z failed to stage current skills: zr:gz/z..z!refusing to extract unsafe path: data)filterz*snapshot extract failed (state restored): zrestored from snapshot r   r   r   r   u   cron links: error — r   r   z  job(s) had skill links restoredz+ backed-up job(s) no longer exist (skipped)z already matchedzcron links: z, z3Curator rollback: restored from %s (cron_report=%s)z; )!r   r(   r}   r   r   r   r   rR   rL   r*   r2   r   r   r   moverA   r   r   r   r   
getmembersr   r   partsr   
extractallr5   r   r1   r3   joinr+   r9   )r   r   ru   r   r   r;   stagedmovedr   r   origr   memberr}   cron_reportsummary_bits
restored_n	skipped_nr   s                      r   rollbackr    s4     Y''F~'+4<'9''''">RS 
 	
 &G>> \T6;TTTVZ[[]]F
LLL---nnGMM$M...
J?&+??@@@@@ J J JBqBBDIIIIIIIJ 7GII777FCTE2222 C C C;;;TBBBBBBBC &(EE&..**++ 	( 	(Ez///EJ&DKE

CII...LL%''''	(  E E E 	 	JD$CIIs4yy1111   	M&55555 	 	 	D	=!==tDDDDDDDEO\'6** 	+b --//  {??3'' 44::3C+C+C!*DDDD   ,D+c&kk&9999 + + +c&kk*****+	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ 	+ W%& O O O 	 	JD$CIIs4yy1111   	M&55555 	 	 	D	GAGGNNNNNNNOfD11111    ,F33K;fk;;<L{## C44:;;
(9::@bAA	??7## 	C OW9M O OPPPP1__aKOOKQR4S4SWX4X4XE N
LLLMMM X	VVVWWW{++ LK 8JJJKKK51A1A ABBB
KKE[* * *$))L))622sd  B0 0
C:CCC$C< <
DDDDBF" "
H.,H)4/G$#H)$
G1.H)0G11H)5HH)
HH)H
H)#H.)H.2L A!L)$KL,K=:L<K==L L LL LL N0.N+6/M&%N+&
M30N+2M33N+7NN+
NN+N
N+%N0+N04O 
OOrb   c                d    dD ](}| dk     s|dk    r|dk    r| dd| n|  dc S | dz  } )| ddS )	N)BKBMBGBi   r
  r  z.1f z Bz GBr   )rb   units     r   format_sizer    sk    '  t88tt||(,a$$$d$$$AAAA	T	====r   c                    t                      } | sdS dddddddddd	d
g}|                    dt          |d                   z             | D ]}|                    |                    dd          dd|                    dd          pdd d         dd|                    dd          ddt	          t          |                    dd                              d
           d                    |          S )NzNo curator snapshots yet.rs   z<24z  r"   z<40r   z>6sizez>8u   ─r   ?(   rw   rv   
)r   r   r3   r1   r  r[   r   )rowslinesrs      r   summarize_backupsr    sH   >>D +**FFFHFFFHFFF6FFFGE	LLU1X&''' 
 
uuT#& @ @hs##*sCRC08@ @uu]A&&-@ @ 3quu_a8899::?@ @	
 	
 	
 	
 99Ur   )r   r   )r   r   r   r   )N)r?   r@   r   rA   )r   r   )r   rW   )r   r[   )rd   r   r   r[   )r   r   r"   rA   rm   r   rn   r[   ro   rp   r   rq   )r   )r"   rA   r   r   )r]   r[   r   r   )r   r   r   r   )r   r   )r   r   r   r   )r   r   r   r   )r   r   r   r   )rb   r[   r   rA   )r   rA   )2__doc__
__future__r   r-   loggingosrer   r   tempfiletimer   r   pathlibr   typingr   r   r	   r
   r   hermes_constantsr   	getLogger__name__r+   r_   r   compiler   r   r   r   r6   r>   rL   rV   rZ   rc   rl   r   r   r   r   r   r   r   r  r  r  r   r   r   <module>r$     s  # #J # " " " " "   				 				     ' ' ' ' ' ' ' '       3 3 3 3 3 3 3 3 3 3 3 3 3 3 , , , , , ,		8	$	$ 
 )&1 
 
F	G	G= = = =( ( ( (4 4 4 4
 & + + + +\% % % % %. . . . 5 5 5 5
       ;?    2F F F F FR" " " "R      :1 1 1 1,K K K K^J3 J3 J3 J3 J3b        r   