Moved some files around and made cleanup better
This commit is contained in:
@@ -54,10 +54,13 @@ def resolve_folder_name(card_id):
|
||||
|
||||
def revision_candidates(name,revision):
|
||||
candidates=[REPO/'in-progress/cards'/name/'revisions'/revision,
|
||||
REPO/'artifacts/cards'/name,
|
||||
REPO/'in-progress/cards'/name/'history'/f'accepted-{revision}']
|
||||
REPO/'artifacts/cards'/name]
|
||||
return [path for path in candidates if (path/'manifest.json').is_file() and read(path/'manifest.json')['revision']==revision]
|
||||
|
||||
def historical_revision_paths(name,revision):
|
||||
base=REPO/'in-progress/cards'/name/'history'
|
||||
return [base/'revisions'/revision,base/f'accepted-{revision}']
|
||||
|
||||
def remap_content(directory,source,destination):
|
||||
path=directory/'card.json'
|
||||
content=read(path)
|
||||
@@ -87,6 +90,96 @@ def rebase_linked_inputs(directory,source,destination):
|
||||
if changed:tree.write(path,encoding='unicode')
|
||||
remap_content(directory,source,destination)
|
||||
|
||||
def internalize_linked_inputs(directory,source):
|
||||
"""Copy live inputs out of a package that is about to become compact history."""
|
||||
source=source.resolve();replacements={};changed=False
|
||||
def is_source_file(path):
|
||||
path=path.resolve()
|
||||
return path.is_file() and path.is_relative_to(source)
|
||||
for path in sorted(directory.rglob('*')):
|
||||
if not path.is_symlink():continue
|
||||
target=path.resolve()
|
||||
if not is_source_file(target):continue
|
||||
path.unlink();shutil.copy2(target,path);replacements[target]=path;changed=True
|
||||
def local_copy(target):
|
||||
nonlocal changed
|
||||
target=target.resolve()
|
||||
if target in replacements:return replacements[target]
|
||||
preferred=directory/'source'/target.name
|
||||
if preferred.exists() and preferred.is_file() and digest(preferred)==digest(target):
|
||||
replacements[target]=preferred;return preferred
|
||||
if preferred.exists():
|
||||
source_revision=read(source/'manifest.json')['revision']
|
||||
preferred=directory/'source'/'retained'/source_revision/target.relative_to(source)
|
||||
preferred.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(target,preferred)
|
||||
replacements[target]=preferred;changed=True;return preferred
|
||||
content_path=directory/'card.json';content=read(content_path);content_changed=False
|
||||
for key in ['art','previousNormal']:
|
||||
value=content.get(key)
|
||||
if not value:continue
|
||||
target=(REPO/value).resolve()
|
||||
if is_source_file(target):
|
||||
content[key]=str(local_copy(target).relative_to(REPO));content_changed=True
|
||||
if content_changed:write(content_path,content)
|
||||
for path in sorted(directory.rglob('*.svg')):
|
||||
tree=ET.parse(path);svg_changed=False
|
||||
for e in tree.getroot().iter():
|
||||
for attr in ['href','{http://www.w3.org/1999/xlink}href']:
|
||||
value=e.get(attr)
|
||||
if not value or value.startswith(('data:','#')):continue
|
||||
target=(path.parent/value).resolve()
|
||||
if is_source_file(target):
|
||||
e.set(attr,os.path.relpath(local_copy(target),path.parent));svg_changed=True
|
||||
if svg_changed:
|
||||
ET.register_namespace('','http://www.w3.org/2000/svg');ET.register_namespace('xlink','http://www.w3.org/1999/xlink')
|
||||
tree.write(path,encoding='unicode');changed=True
|
||||
return changed
|
||||
|
||||
def compact_revision(directory,reason):
|
||||
"""Retain text provenance and one low reference face per printing."""
|
||||
manifest_path=directory/'manifest.json'
|
||||
if not manifest_path.is_file():raise ValueError(f'Cannot compact a revision without a manifest: {directory}')
|
||||
manifest=read(manifest_path);full_manifest=directory/'full-manifest.json'
|
||||
if full_manifest.exists():raise ValueError(f'Revision is already compact: {directory}')
|
||||
references=sorted((directory/'low').glob('*/card.png'))
|
||||
if not references:
|
||||
art=directory/'source/art-master.png'
|
||||
if art.is_file():
|
||||
preview=directory/'review/art-reference.png';preview.parent.mkdir(parents=True,exist_ok=True)
|
||||
with Image.open(art) as image:
|
||||
image=image.convert('RGBA');image.thumbnail(RESOLUTIONS['low'],Image.Resampling.LANCZOS)
|
||||
image.save(preview,optimize=True)
|
||||
references=[preview]
|
||||
manifest_path.replace(full_manifest)
|
||||
for resolution in ['med','high']:
|
||||
path=directory/resolution
|
||||
if path.exists():shutil.rmtree(path)
|
||||
low=directory/'low'
|
||||
if low.exists():
|
||||
for path in sorted(low.rglob('*'),reverse=True):
|
||||
if path.is_file() or path.is_symlink():
|
||||
if not (path.name=='card.png' and path.parent.name in PRINTINGS):path.unlink()
|
||||
elif path.is_dir() and not any(path.iterdir()):path.rmdir()
|
||||
visual_suffixes={'.png','.jpg','.jpeg','.webp','.tif','.tiff','.gif','.bmp','.svg','.psd','.xcf'}
|
||||
for area in ['source','review']:
|
||||
root=directory/area
|
||||
if not root.exists():continue
|
||||
for path in sorted(root.rglob('*'),reverse=True):
|
||||
if (path.is_file() or path.is_symlink()) and path.suffix.lower() in visual_suffixes and path not in references:path.unlink()
|
||||
elif path.is_dir() and not any(path.iterdir()):path.rmdir()
|
||||
retained_references=[str(path.relative_to(directory)) for path in references if path.is_file()]
|
||||
record={'schemaVersion':1,'kind':'compact-card-revision','cardId':manifest['cardId'],
|
||||
'folderName':manifest.get('folderName'),'revision':manifest['revision'],'originalStage':manifest['stage'],
|
||||
'approval':manifest.get('approval'),'reason':reason,'originalManifest':'full-manifest.json',
|
||||
'originalManifestSHA256':digest(full_manifest),'referenceResolution':'low','referenceImages':retained_references,
|
||||
'retention':'Text provenance plus one 500 x 700 card face per available printing; full current acceptance lives in artifacts/cards.'}
|
||||
write(directory/'record.json',record);return record
|
||||
|
||||
def add_history_entry(profile,revision,path,status):
|
||||
base=REPO/'in-progress/cards'/profile['folderName'];relative=str(path.relative_to(base))
|
||||
if not any(entry.get('revision')==revision and entry.get('path')==relative for entry in profile.setdefault('history',[])):
|
||||
profile['history'].append({'revision':revision,'path':relative,'status':status})
|
||||
|
||||
def resolve_revision(card_id,revision):
|
||||
safe_card_id(card_id);safe_name(revision);name=resolve_folder_name(card_id)
|
||||
found=revision_candidates(name,revision)
|
||||
@@ -101,7 +194,8 @@ def scaffold(card_id,title,rarity,revision):
|
||||
existing=any(read(path)['cardId']==card_id for root in roots for path in profile_paths(root))
|
||||
name=resolve_folder_name(card_id) if existing else f'{card_id}-{title_slug(title)}'
|
||||
base=roots[0]/name;target=base/'revisions'/revision
|
||||
if target.exists() or revision_candidates(name,revision):raise ValueError('Revision already exists; choose a new revision.')
|
||||
if target.exists() or revision_candidates(name,revision) or any(path.exists() for path in historical_revision_paths(name,revision)):
|
||||
raise ValueError('Revision already exists; choose a new revision.')
|
||||
for folder in ['source','review']:(target/folder).mkdir(parents=True,exist_ok=True)
|
||||
for resolution in RESOLUTIONS:
|
||||
for printing in PRINTINGS:(target/resolution/printing).mkdir(parents=True)
|
||||
@@ -209,33 +303,71 @@ def relocate_revision(source,destination,aliases):
|
||||
|
||||
def approve(card_id,revision,note):
|
||||
if not note.strip():raise ValueError('Record the user’s explicit approval wording')
|
||||
name=resolve_folder_name(card_id)
|
||||
profile_path=REPO/'in-progress/cards'/name/'card.json'
|
||||
name=resolve_folder_name(card_id);base=REPO/'in-progress/cards'/name
|
||||
profile_path=base/'card.json'
|
||||
if read(profile_path)['selectedRevision']!=revision:raise ValueError('Select this exact revision in the card profile before approving it')
|
||||
source=resolve_revision(card_id,revision);manifest=validate(source)
|
||||
if manifest['stage']!='assembly-review':raise ValueError('Only an assembly-review revision can be approved')
|
||||
aliases=manifest.get('compatibilityAliases',[])
|
||||
for alias in aliases:
|
||||
if not (REPO/alias['alias']).is_symlink():raise ValueError('Compatibility alias no longer exists')
|
||||
target=REPO/'artifacts/cards'/name
|
||||
siblings=sorted(path for path in (base/'revisions').glob('*') if path.is_dir() and path!=source)
|
||||
sibling_archives=[base/'history/revisions'/path.name for path in siblings]
|
||||
if any(path.exists() for path in sibling_archives):raise ValueError('Superseded revision history destination already exists')
|
||||
target=REPO/'artifacts/cards'/name;previous=None;archive=None
|
||||
if target.exists():
|
||||
previous=validate(target)
|
||||
if previous['stage']!='approved':raise ValueError('Existing artifact is not an approved card')
|
||||
archive=REPO/'in-progress/cards'/name/'history'/f"accepted-{previous['revision']}"
|
||||
archive=base/'history'/f"accepted-{previous['revision']}"
|
||||
if archive.exists():raise ValueError('Previous acceptance archive already exists')
|
||||
# Pin incoming references to the old accepted artwork before replacing its location.
|
||||
rebase_linked_inputs(source,target,archive)
|
||||
changed=False
|
||||
for dependency in siblings+([target] if target.exists() else []):
|
||||
changed=internalize_linked_inputs(source,dependency) or changed
|
||||
if changed:
|
||||
manifest=refresh(source);validate(source)
|
||||
if target.exists():
|
||||
relocate_revision(target,archive,previous.get('compatibilityAliases',[]))
|
||||
refresh(archive,allow_approved=True)
|
||||
refresh(archive,allow_approved=True);compact_revision(archive,'replaced by a newer accepted revision')
|
||||
target=relocate_revision(source,target,aliases)
|
||||
manifest['stage']='approved';manifest['approval']={'by':'user','note':note};write(target/'manifest.json',manifest)
|
||||
base=REPO/'in-progress/cards'/name;profile=read(base/'card.json');profile.update({'selectedRevision':revision,'stage':'approved','approval':manifest['approval']});write(base/'card.json',profile)
|
||||
profile=read(profile_path);profile.update({'selectedRevision':revision,'stage':'approved','approval':manifest['approval']})
|
||||
if archive is not None:add_history_entry(profile,previous['revision'],archive,'superseded-acceptance-record')
|
||||
for sibling,destination in zip(siblings,sibling_archives):
|
||||
destination.parent.mkdir(parents=True,exist_ok=True);shutil.move(str(sibling),str(destination))
|
||||
compact_revision(destination,f'superseded when {revision} was accepted')
|
||||
add_history_entry(profile,read(destination/'record.json')['revision'],destination,'superseded-revision-record')
|
||||
write(profile_path,profile)
|
||||
content=read(target/'card.json')
|
||||
for key in ['schemaVersion','selectedRevision','stage','approval','rarity']:
|
||||
if key in profile:content[key]=profile[key]
|
||||
write(target/'card.json',content)
|
||||
refresh(target,allow_approved=True);update_index();return target
|
||||
|
||||
def compact_card_history(card_id):
|
||||
"""Apply compact retention to history for a card that is already approved."""
|
||||
name=resolve_folder_name(card_id);base=REPO/'in-progress/cards'/name;profile_path=base/'card.json'
|
||||
profile=read(profile_path);target=REPO/'artifacts/cards'/name
|
||||
manifest=validate(target)
|
||||
if manifest['stage']!='approved' or profile.get('stage')!='approved':raise ValueError('History cleanup requires an approved card')
|
||||
accepted=sorted(path for path in (base/'history').glob('accepted-*') if (path/'manifest.json').is_file())
|
||||
working=sorted(path for path in (base/'revisions').glob('*') if (path/'manifest.json').is_file())
|
||||
destinations=[base/'history/revisions'/path.name for path in working]
|
||||
if any(path.exists() for path in destinations):raise ValueError('Superseded revision history destination already exists')
|
||||
changed=False
|
||||
for dependency in accepted+working:changed=internalize_linked_inputs(target,dependency) or changed
|
||||
if changed:refresh(target,allow_approved=True)
|
||||
validate(target)
|
||||
records=[]
|
||||
for archive in accepted:
|
||||
record=compact_revision(archive,'replaced by the current accepted revision');records.append(record)
|
||||
add_history_entry(profile,record['revision'],archive,'superseded-acceptance-record')
|
||||
for source,destination in zip(working,destinations):
|
||||
destination.parent.mkdir(parents=True,exist_ok=True);shutil.move(str(source),str(destination))
|
||||
record=compact_revision(destination,f"superseded when {manifest['revision']} was accepted");records.append(record)
|
||||
add_history_entry(profile,record['revision'],destination,'superseded-revision-record')
|
||||
write(profile_path,profile);update_index()
|
||||
return {'cardId':card_id,'acceptedRevision':manifest['revision'],'compacted':[record['revision'] for record in records]}
|
||||
|
||||
def update_index():
|
||||
profiles={}
|
||||
# Working selection takes precedence when an older approved release also exists.
|
||||
@@ -264,12 +396,14 @@ if __name__=='__main__':
|
||||
p=sub.add_parser(command);p.add_argument('--card',required=True);p.add_argument('--revision',required=True)
|
||||
if command=='new':p.add_argument('--title',required=True);p.add_argument('--rarity',choices=['Common','Uncommon','Rare','Extraordinary','Legendary'],required=True)
|
||||
if command=='approve':p.add_argument('--approval-note',required=True)
|
||||
compact_parser=sub.add_parser('compact');compact_parser.add_argument('--card',required=True)
|
||||
sub.add_parser('status');args=parser.parse_args()
|
||||
try:
|
||||
if args.command=='status':
|
||||
for card in update_index():print(f'{card["cardId"]} · {card["title"]}: {card["stage"]} · {card["selectedRevision"]} · {card["path"]}')
|
||||
elif args.command=='new':print(scaffold(args.card,args.title,args.rarity,args.revision))
|
||||
elif args.command=='approve':print(approve(args.card,args.revision,args.approval_note))
|
||||
elif args.command=='compact':print(json.dumps(compact_card_history(args.card),indent=2))
|
||||
elif args.command=='refresh':print(refresh(resolve_revision(args.card,args.revision))['stage']);update_index()
|
||||
else:print(validate(resolve_revision(args.card,args.revision))['stage'])
|
||||
except (ValueError,FileNotFoundError,KeyError) as error:parser.exit(1,f'{error}\n')
|
||||
|
||||
Reference in New Issue
Block a user