Moved some files around and made cleanup better

This commit is contained in:
2026-09-14 19:01:08 -07:00
parent 50b234104a
commit 97ed7f05c8
866 changed files with 404 additions and 1612 deletions

View File

@@ -6,7 +6,7 @@ The immutable [recipe](recipes/raw-ridges-v1.json) pins all parameters and is ha
For reusable frame/backing inputs, load a pinned version from the [shared tier layer library](../../artifacts/layers/README.md). The [four-tier comparison](../../spikes/tier-layer-library-proof/README.md) demonstrates shared layers and this finish module together without migrating historical fixtures.
`card_workspace.py` manages the [working-card layout, progress index and approval move](../../docs/card-workspace.md). Use `new`, `refresh`, `validate`, and `status` during production. Run `approve` only after explicit final approval of an exact card revision; this organization migration does not approve or install any cards.
`card_workspace.py` manages the [working-card layout, progress index and approval move](../../docs/card-workspace.md). Use `new`, `refresh`, `validate`, and `status` during production. Run `approve` only after explicit final approval of an exact card revision; it keeps the full accepted package in `artifacts/cards/` and compacts superseded work to textual provenance plus low printing references. `compact --card ID` applies that retention once to an already approved card. Neither command installs harness fixtures.
## One printing from the CLI
@@ -79,7 +79,7 @@ Tests cover source-edge handling, dark colored lines, broad shadow steps, canvas
It analyzes fitted artwork once through `raw-ridges-v1`, composes all four printings, derives matching finish/text masks, writes low/med/high exports and editable high SVGs, then refreshes the working manifests at assembly-review. Approved card packages are frozen; the tool does not approve cards or install harness fixtures. It requires a layout with the declared central-flourish spacing policy and does not provide a Common or bespoke Legendary layout.
```sh
python3 tools/card-production/assemble_card.py --root in-progress/cards/BE-042-calling-of-the-first-disciples/revisions/v03
python3 tools/card-production/assemble_card.py --root in-progress/cards/ID-title/revisions/v02
```
Read each revision's `review/build-validation.json` and `review/printings-comparison.png` for measured results and visual-review scope.

View File

@@ -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')

View File

@@ -52,10 +52,11 @@ class CardWorkspaceTests(unittest.TestCase):
with self.assertRaises(ValueError):workspace.resolve_revision('BP-999','v02')
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
def test_new_acceptance_archives_previous_and_pins_linked_art(self):
def test_new_acceptance_compacts_previous_and_internalizes_linked_art(self):
alias=self.assemble()
accepted=workspace.approve('BP-999','v01','First fixture approval')
before=workspace.digest(accepted/'med/normal/card.png')
low_before=workspace.digest(accepted/'low/normal/card.png')
linked_before=workspace.digest(accepted/'high/boundless/card.png')
self.revision=workspace.scaffold('BP-999','Test card','Common','v02')
alias.unlink();self.assemble()
# This revision deliberately depends on the earlier accepted full-art export.
@@ -63,15 +64,33 @@ class CardWorkspaceTests(unittest.TestCase):
content=workspace.read(self.revision/'card.json');content['art']=str((accepted/'high/boundless/card.png').relative_to(self.repo));workspace.write(self.revision/'card.json',content);workspace.refresh(self.revision)
target=workspace.approve('BP-999','v02','Second fixture approval')
archive=self.repo/'in-progress/cards/BP-999-test-card/history/accepted-v01'
self.assertEqual(workspace.resolve_revision('BP-999','v01'),archive)
with self.assertRaises(ValueError):workspace.resolve_revision('BP-999','v01')
self.assertEqual(workspace.resolve_revision('BP-999','v02'),target)
self.assertEqual(workspace.digest(archive/'med/normal/card.png'),before)
self.assertEqual((target/'source/art-master.png').resolve(),archive/'high/boundless/card.png')
self.assertEqual(workspace.validate(archive)['stage'],'approved')
self.assertEqual(workspace.digest(archive/'low/normal/card.png'),low_before)
self.assertFalse((archive/'med').exists());self.assertFalse((archive/'high').exists())
self.assertTrue((archive/'full-manifest.json').is_file());self.assertTrue((archive/'record.json').is_file())
self.assertFalse((target/'source/art-master.png').is_symlink())
self.assertEqual(workspace.digest(target/'source/art-master.png'),linked_before)
self.assertEqual(workspace.read(target/'card.json')['art'],'artifacts/cards/BP-999-test-card/source/art-master.png')
self.assertEqual(workspace.validate(target)['stage'],'approved')
self.assertEqual(alias.resolve(),target/'med/normal/card.png')
self.assertEqual(workspace.update_index()[0]['selectedRevision'],'v02')
def test_approval_compacts_superseded_working_revisions(self):
old=self.revision
Image.new('RGB',(20,28),'red').save(old/'source/art-master.png')
(old/'source/generation-prompt.txt').write_text('Retained prompt')
workspace.refresh(old)
self.revision=workspace.scaffold('BP-999','Test card','Common','v02');self.assemble()
workspace.approve('BP-999','v02','Second fixture approval')
history=self.repo/'in-progress/cards/BP-999-test-card/history/revisions/v01'
self.assertFalse(old.exists());self.assertTrue((history/'review/art-reference.png').is_file())
with Image.open(history/'review/art-reference.png') as reference:self.assertEqual(reference.size,(5,7))
self.assertFalse((history/'source/art-master.png').exists())
self.assertEqual((history/'source/generation-prompt.txt').read_text(),'Retained prompt')
self.assertTrue((history/'full-manifest.json').is_file())
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
def test_changed_or_invalid_exports_block_promotion(self):
self.assemble();Image.new('RGBA',(9,14),(40,60,80,255)).save(self.revision/'med/normal/card.png')
with self.assertRaises(ValueError):workspace.approve('BP-999','v01','Test fixture approval only')