@@ -0,0 +1,411 @@
""" Initialize three ordinary resource copies from a frozen Git commit.
Run from the primary repository. This script creates no branches or worktrees,
does not launch Unity/Blender, and never overwrites an existing resource copy.
"""
import argparse
import ast
from datetime import datetime , timedelta , timezone
import hashlib
import json
from pathlib import Path , PurePosixPath
import re
import shutil
import stat
import subprocess
import uuid
import zipfile
ROOT = Path ( __file__ ) . resolve ( ) . parents [ 2 ]
NAMES = ( ' tree1 ' , ' tree2 ' , ' tree3 ' )
ADAPTED = (
' Tools/Unity/Commands/Build.json ' ,
' Tools/Unity/Commands/LocalizationBuild.json ' ,
' Tools/Unity/Commands/Restart.json ' ,
' Tools/Blender/CreatePrototypeMech.py ' ,
)
GENERATED = ( ' WORKSPACE.md ' , ' WorkspaceInfo.json ' , ' InitializationManifest.json ' )
FORBIDDEN = { ' .git ' , ' library ' , ' temp ' , ' obj ' , ' logs ' , ' usersettings ' , ' builds ' , ' .vs ' , ' .idea ' , ' memorycaptures ' , ' recordings ' }
HK = timezone ( timedelta ( hours = 8 ) , ' Asia/Hong_Kong ' )
def git ( * args , check = True ) :
return subprocess . run ( [ ' git ' , ' -C ' , str ( ROOT ) , * args ] , capture_output = True , check = check )
def git_text ( * args ) :
return git ( * args ) . stdout . decode ( ' utf-8 ' ) . strip ( )
def sha ( data ) :
return hashlib . sha256 ( data ) . hexdigest ( )
def file_sha ( path ) :
with path . open ( ' rb ' ) as stream :
return hashlib . file_digest ( stream , ' sha256 ' ) . hexdigest ( )
def safe_child ( base , path ) :
""" Resolve paths before any directory publication/move or file creation. """
base = base . resolve ( )
resolved = path . resolve ( )
if resolved == base or not resolved . is_relative_to ( base ) :
raise RuntimeError ( f ' Path leaves the allowed directory: { path } ' )
return resolved
def is_link ( path ) :
attributes = getattr ( path . lstat ( ) , ' st_file_attributes ' , 0 )
return path . is_symlink ( ) or bool ( attributes & getattr ( stat , ' FILE_ATTRIBUTE_REPARSE_POINT ' , 0x400 ) )
def write_json ( path , data ) :
path . write_text ( json . dumps ( data , ensure_ascii = False , indent = 2 ) + ' \n ' , encoding = ' utf-8 ' )
def main_assets_state ( ) :
return { p . relative_to ( ROOT ) . as_posix ( ) : file_sha ( p ) for p in sorted ( ( ROOT / ' Assets ' ) . rglob ( ' * ' ) ) if p . is_file ( ) }
def git_state ( ) :
return {
' head ' : git_text ( ' rev-parse ' , ' HEAD ' ) ,
' branches ' : git_text ( ' for-each-ref ' , ' --format= %(refname) % (objectname) ' , ' refs/heads ' ) ,
' worktrees ' : git_text ( ' worktree ' , ' list ' , ' --porcelain ' ) ,
' index ' : git_text ( ' ls-files ' , ' --stage ' ) ,
}
def baseline_files ( commit , run ) :
modes = { }
for record in git ( ' ls-tree ' , ' -r ' , ' -z ' , commit ) . stdout . split ( b ' \0 ' ) :
if not record :
continue
meta , raw_path = record . split ( b ' \t ' , 1 )
mode , kind , _ = meta . decode ( ' ascii ' ) . split ( )
name = raw_path . decode ( ' utf-8 ' )
path = PurePosixPath ( name )
if path . is_absolute ( ) or ' .. ' in path . parts or ' \\ ' in name or ' : ' in name :
raise RuntimeError ( f ' Invalid archive path: { name } ' )
if path . parts [ 0 ] . lower ( ) in FORBIDDEN or path . parts [ 0 ] in NAMES :
raise RuntimeError ( f ' Generated directory is tracked in baseline: { name } ' )
if mode not in ( ' 100644 ' , ' 100755 ' ) or kind != ' blob ' :
raise RuntimeError ( f ' Only ordinary files are supported: { mode } { name } ' )
modes [ name ] = mode
archive_path = run / ' baseline.zip '
with archive_path . open ( ' xb ' ) as stream :
subprocess . run ( [ ' git ' , ' -C ' , str ( ROOT ) , ' archive ' , ' --format=zip ' , commit ] , stdout = stream , check = True )
with zipfile . ZipFile ( archive_path ) as archive :
files = { i . filename : archive . read ( i ) for i in archive . infolist ( ) if not i . is_dir ( ) }
if set ( files ) != set ( modes ) :
raise RuntimeError ( ' Archive does not match the frozen Git tree. ' )
if set ( GENERATED ) & set ( files ) :
raise RuntimeError ( ' Workspace metadata would overwrite baseline files. ' )
for name in ADAPTED :
if name not in files :
raise RuntimeError ( f ' Expected tool is missing: { name } ' )
return files
def source_root ( files ) :
text = files [ ADAPTED [ 3 ] ] . decode ( ' utf-8 ' )
match = re . search ( r " ^root_path = Path \ ( ' ([^ ' ]+) ' \ )$ " , text , re . MULTILINE )
if not match :
raise RuntimeError ( ' The Blender root constant is not recognized. ' )
return match . group ( 1 )
def adapted_files ( files , destination ) :
before_root = source_root ( files )
after_root = destination . as_posix ( )
output = dict ( files )
records = [ ]
for name in ADAPTED :
if name . endswith ( ' .json ' ) :
commands = json . loads ( files [ name ] . decode ( ' utf-8 ' ) )
if len ( commands ) != 1 :
raise RuntimeError ( f ' Expected one command in { name } ' )
arguments = commands [ 0 ] [ ' arguments ' ]
if name . endswith ( ' /Restart.json ' ) :
previous = arguments [ ' code ' ]
expected = f ' UnityEditor.EditorApplication.OpenProject( " { before_root } " ) '
replacement = f ' UnityEditor.EditorApplication.OpenProject( " { after_root } " ) '
if previous . count ( expected ) != 1 :
raise RuntimeError ( f ' Unexpected restart target in { name } ' )
arguments [ ' code ' ] = previous . replace ( expected , replacement )
clone = json . loads ( json . dumps ( commands ) )
clone [ 0 ] [ ' arguments ' ] [ ' code ' ] = clone [ 0 ] [ ' arguments ' ] [ ' code ' ] . replace ( replacement , expected )
else :
previous = arguments [ ' output_path ' ]
relative = PurePosixPath ( previous ) . relative_to ( PurePosixPath ( before_root ) )
if relative . parts [ 0 ] != ' Builds ' or ' .. ' in relative . parts :
raise RuntimeError ( f ' Unexpected build output in { name } ' )
arguments [ ' output_path ' ] = destination . joinpath ( * relative . parts ) . as_posix ( )
safe_child ( destination , Path ( arguments [ ' output_path ' ] ) )
clone = json . loads ( json . dumps ( commands ) )
clone [ 0 ] [ ' arguments ' ] [ ' output_path ' ] = previous
if clone != json . loads ( files [ name ] . decode ( ' utf-8 ' ) ) :
raise RuntimeError ( f ' Unexpected semantic change to { name } ' )
output [ name ] = ( json . dumps ( commands , ensure_ascii = False , indent = 2 ) + ' \n ' ) . encode ( ' utf-8 ' )
else :
previous = files [ name ] . decode ( ' utf-8 ' )
expected = f " root_path = Path( ' { before_root } ' ) "
replacement = f " root_path = Path( ' { after_root } ' ) "
if previous . count ( expected ) != 1 :
raise RuntimeError ( ' Unexpected Blender root constant. ' )
result = previous . replace ( expected , replacement )
ast . parse ( result , filename = name )
if result . replace ( replacement , expected ) != previous :
raise RuntimeError ( ' Unexpected Blender source edit. ' )
output [ name ] = result . encode ( ' utf-8 ' )
if output [ name ] == files [ name ] :
raise RuntimeError ( f ' Expected a path adaptation: { name } ' )
records . append ( { ' path ' : name , ' fromRoot ' : before_root , ' toRoot ' : after_root ,
' sourceSha256 ' : sha ( files [ name ] ) , ' sha256 ' : sha ( output [ name ] ) } )
return output , records
def workspace_info ( name , destination , commit , initialized ) :
return {
' schemaVersion ' : 1 , ' name ' : name , ' stage ' : ' resources-only ' , ' initializedAt ' : initialized ,
' sourceRepository ' : ROOT . as_posix ( ) , ' sourceCommit ' : commit ,
' projectRoot ' : destination . as_posix ( ) , ' unityVersion ' : ' 6000.6.8f1 ' ,
' git ' : { ' kind ' : ' ordinary-directory ' , ' branchCreated ' : False , ' worktreeRegistered ' : False } ,
' mcp ' : { ' expectedProjectRoot ' : destination . as_posix ( ) , ' instanceId ' : None , ' connected ' : False } ,
' runtimeDataIsolation ' : { ' status ' : ' pending-before-play ' ,
' reason ' : ' The copied companyName/productName and PlayerPrefs namespace still match the baseline. ' } ,
' unityImport ' : ' not-run ' , ' unityCompilation ' : ' not-run ' , ' unityBuild ' : ' not-run ' ,
}
def create_copy ( staging , destination , name , files , commit , initialized ) :
staging . mkdir ( )
output , adaptations = adapted_files ( files , destination )
for relative , data in output . items ( ) :
target = safe_child ( staging , staging / relative )
target . parent . mkdir ( parents = True , exist_ok = True )
with target . open ( ' xb ' ) as stream :
stream . write ( data )
# Git cannot store empty folders; Unity folder metadata can describe one.
for relative , data in files . items ( ) :
if relative . startswith ( ' Assets/ ' ) and relative . endswith ( ' .meta ' ) and re . search ( rb ' ^folderAsset: \ s*yes \ s*$ ' , data , re . MULTILINE ) :
safe_child ( staging , staging / relative [ : - 5 ] ) . mkdir ( parents = True , exist_ok = True )
write_json ( staging / ' WorkspaceInfo.json ' , workspace_info ( name , destination , commit , initialized ) )
( staging / ' WORKSPACE.md ' ) . write_text (
f ' # { name } 资源目录 \n \n '
f ' 基线:` { commit } `。Unity: `6000.6.8f1`。目标:` { destination . as_posix ( ) } `。 \n \n '
' 本目录是普通资源副本,未建立独立 Git 仓库、分支或 worktree。 '
' 目录内部的 Git 命令可能识别到父仓库,不应作为独立版本记录使用。 \n \n '
' 资源、GUID、制作源文件与许可已复制;四份工具的输出或重开路径已指向本目录。 '
' 路径适配及逐文件校验见 InitializationManifest.json。历史文档中的路径仍记录原来源。 \n \n '
' MCP 尚未连接,Unity 尚未导入、编译或构建。首次使用需要为本目录选择明确的 Unity 实例与完整路径。 '
' 默认存档和 PlayerPrefs 命名空间尚未隔离,完成隔离前不要并行运行游戏。 \n \n '
' 后续按主工程 Docs/Production/TreeInitializationPlan.md 迁移至正式开发工作区。 \n ' ,
encoding = ' utf-8 ' )
generated = [ { ' path ' : relative , ' bytes ' : ( staging / relative ) . stat ( ) . st_size ,
' sha256 ' : file_sha ( staging / relative ) } for relative in GENERATED [ : 2 ] ]
manifest = {
' schemaVersion ' : 1 , ' sourceCommit ' : commit , ' projectRoot ' : destination . as_posix ( ) ,
' sourceFileCount ' : len ( files ) , ' sourceBytes ' : sum ( map ( len , files . values ( ) ) ) ,
' adaptations ' : adaptations ,
' files ' : [ { ' path ' : path , ' bytes ' : len ( data ) , ' sourceSha256 ' : sha ( files [ path ] ) ,
' sha256 ' : sha ( data ) , ' adapted ' : path in ADAPTED } for path , data in sorted ( output . items ( ) ) ] ,
' generatedFiles ' : generated ,
' selfHash ' : ' Recorded in the primary repository initialization report, not inside this manifest. ' ,
}
write_json ( staging / ' InitializationManifest.json ' , manifest )
def validate_copy ( directory , destination , name , files , commit ) :
output , adaptations = adapted_files ( files , destination )
metadata = json . loads ( ( directory / ' WorkspaceInfo.json ' ) . read_text ( encoding = ' utf-8 ' ) )
if metadata [ ' name ' ] != name or metadata [ ' sourceCommit ' ] != commit or metadata [ ' projectRoot ' ] != destination . as_posix ( ) :
raise RuntimeError ( f ' Existing workspace has a different identity: { directory } ' )
if metadata [ ' stage ' ] != ' resources-only ' or metadata [ ' git ' ] != { ' kind ' : ' ordinary-directory ' , ' branchCreated ' : False , ' worktreeRegistered ' : False } :
raise RuntimeError ( f ' Workspace has advanced beyond resource initialization: { directory } ' )
if metadata [ ' mcp ' ] != { ' expectedProjectRoot ' : destination . as_posix ( ) , ' instanceId ' : None , ' connected ' : False } :
raise RuntimeError ( f ' Workspace connection state changed: { directory } ' )
manifest = json . loads ( ( directory / ' InitializationManifest.json ' ) . read_text ( encoding = ' utf-8 ' ) )
if manifest [ ' sourceCommit ' ] != commit or manifest [ ' projectRoot ' ] != destination . as_posix ( ) or manifest [ ' adaptations ' ] != adaptations :
raise RuntimeError ( f ' Manifest baseline or adaptations differ: { directory } ' )
if manifest [ ' sourceFileCount ' ] != len ( files ) or manifest [ ' sourceBytes ' ] != sum ( map ( len , files . values ( ) ) ) :
raise RuntimeError ( f ' Manifest baseline count or size differs: { directory } ' )
recorded = { entry [ ' path ' ] : entry for entry in manifest [ ' files ' ] }
if len ( recorded ) != len ( files ) or set ( recorded ) != set ( output ) :
raise RuntimeError ( f ' Manifest file set differs: { directory } ' )
found = [ ]
for path in directory . rglob ( ' * ' ) :
safe_child ( directory , path )
if is_link ( path ) :
raise RuntimeError ( f ' Linked path is not allowed: { path } ' )
if path . is_file ( ) :
if path . stat ( ) . st_nlink != 1 :
raise RuntimeError ( f ' Hard-linked file is not allowed: { path } ' )
found . append ( path . relative_to ( directory ) . as_posix ( ) )
if set ( found ) != set ( files ) | set ( GENERATED ) :
raise RuntimeError ( f ' Unexpected or missing files in { directory } ' )
if any ( ( directory / item ) . exists ( ) for item in ( ' .git ' , ' Library ' , ' Temp ' , ' Obj ' , ' Logs ' , ' UserSettings ' , ' Builds ' , ' .vs ' , ' .idea ' ) ) :
raise RuntimeError ( f ' Generated directory was copied: { directory } ' )
for relative , expected in output . items ( ) :
path = directory / relative
entry = recorded [ relative ]
if entry != { ' path ' : relative , ' bytes ' : len ( expected ) , ' sourceSha256 ' : sha ( files [ relative ] ) ,
' sha256 ' : sha ( expected ) , ' adapted ' : relative in ADAPTED } :
raise RuntimeError ( f ' Manifest hash is not derived from the baseline: { relative } ' )
if path . stat ( ) . st_size != len ( expected ) or file_sha ( path ) != sha ( expected ) :
raise RuntimeError ( f ' File does not match baseline/adaptation: { path } ' )
if relative . endswith ( ' .json ' ) :
json . loads ( path . read_text ( encoding = ' utf-8-sig ' ) )
if relative . startswith ( ' Tools/ ' ) and relative . endswith ( ' .py ' ) :
ast . parse ( path . read_text ( encoding = ' utf-8-sig ' ) , filename = relative )
if { entry [ ' path ' ] for entry in manifest [ ' generatedFiles ' ] } != set ( GENERATED [ : 2 ] ) or len ( manifest [ ' generatedFiles ' ] ) != 2 :
raise RuntimeError ( ' Generated metadata records differ. ' )
for entry in manifest [ ' generatedFiles ' ] :
path = directory / entry [ ' path ' ]
if path . stat ( ) . st_size != entry [ ' bytes ' ] or file_sha ( path ) != entry [ ' sha256 ' ] :
raise RuntimeError ( f ' Generated metadata was changed: { path } ' )
assets = directory / ' Assets '
meta_count = 0
guids = set ( )
for path in assets . rglob ( ' * ' ) :
if path . name . endswith ( ' .meta ' ) :
meta_count + = 1
if not path . with_suffix ( ' ' ) . exists ( ) :
raise RuntimeError ( f ' Orphan Unity metadata: { path } ' )
match = re . search ( r ' ^guid: ([a-f0-9] {32} )$ ' , path . read_text ( encoding = ' utf-8 ' ) , re . MULTILINE )
if not match or match . group ( 1 ) in guids :
raise RuntimeError ( f ' Missing or duplicate Unity GUID: { path } ' )
guids . add ( match . group ( 1 ) )
elif not Path ( str ( path ) + ' .meta ' ) . is_file ( ) :
raise RuntimeError ( f ' Unity asset has no metadata: { path } ' )
required = (
' Assets/Scenes/Bootstrap.unity ' , ' Assets/Scenes/MechTestRange.unity ' , ' Assets/Scenes/WarzonePrototype.unity ' ,
' Assets/Game/UI/Fonts/GameFont.asset ' , ' Assets/Game/UI/Fonts/GameTextSettings.asset ' ,
' Assets/Game/UI/GamePanelSettings.asset ' , ' Assets/Game/Localization/LocalizationCatalog.asset ' ,
' Assets/Game/Localization/Tables/en.json ' , ' Assets/Game/Localization/Tables/zh-Hans.json ' ,
' Assets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf ' , ' Assets/ThirdParty/Adobe/NotoSansSC/OFL.txt ' ,
' SourceAssets/Blender/Mechs/Mech_Scout_A.blend ' , ' SourceAssets/ThirdParty/Adobe/NotoSansSC/NotoSansSC-VF.ttf ' ,
' Docs/Production/Licenses/Adobe/NotoSansSC/OFL.txt ' , ' Packages/manifest.json ' , ' Packages/packages-lock.json ' ,
)
if any ( not ( directory / path ) . is_file ( ) for path in required ) :
raise RuntimeError ( ' Required engine resource, source or license is missing. ' )
if ' m_EditorVersion: 6000.6.8f1 ' not in ( directory / ' ProjectSettings/ProjectVersion.txt ' ) . read_text ( ) :
raise RuntimeError ( ' Unexpected Unity version. ' )
tables = [ json . loads ( ( directory / f ' Assets/Game/Localization/Tables/ { code } .json ' ) . read_text ( encoding = ' utf-8 ' ) ) [ ' entries ' ] for code in ( ' en ' , ' zh-Hans ' ) ]
keys = [ { entry [ ' key ' ] for entry in entries } for entries in tables ]
if keys [ 0 ] != keys [ 1 ] or any ( len ( entries ) != len ( key_set ) for entries , key_set in zip ( tables , keys ) ) :
raise RuntimeError ( ' Localization table keys do not match. ' )
return { ' name ' : name , ' path ' : destination . as_posix ( ) , ' status ' : ' passed ' , ' sourceCommit ' : commit ,
' sourceFiles ' : len ( files ) , ' totalFiles ' : len ( found ) , ' bytes ' : sum ( ( directory / item ) . stat ( ) . st_size for item in found ) ,
' adaptedFiles ' : len ( ADAPTED ) , ' unchangedFiles ' : len ( files ) - len ( ADAPTED ) ,
' unityMetaFiles ' : meta_count , ' uniqueUnityGuids ' : len ( guids ) , ' localizationKeysPerLocale ' : len ( keys [ 0 ] ) ,
' independentOrdinaryFiles ' : True , ' cacheAndGitExcluded ' : True , ' hashValidation ' : ' passed ' ,
' manifestSha256 ' : file_sha ( directory / ' InitializationManifest.json ' ) }
def main ( ) :
parser = argparse . ArgumentParser ( description = __doc__ )
parser . add_argument ( ' --base ' , default = ' HEAD ' , help = ' HEAD or a hexadecimal Git commit ID. ' )
args = parser . parse_args ( )
if not re . fullmatch ( r ' HEAD|[a-fA-F0-9] { 7,40} ' , args . base ) :
raise RuntimeError ( ' Use HEAD or a hexadecimal commit ID for --base. ' )
if Path ( git_text ( ' rev-parse ' , ' --show-toplevel ' ) ) . resolve ( ) != ROOT :
raise RuntimeError ( ' Run this tool from the primary repository, not a resource copy. ' )
commit = git_text ( ' rev-parse ' , ' --verify ' , args . base + ' ^ {commit} ' )
state_before = git_state ( )
assets_before = main_assets_state ( )
targets = { }
for name in NAMES :
raw = ROOT / name
if raw . exists ( ) or raw . is_symlink ( ) :
if is_link ( raw ) or not raw . is_dir ( ) :
raise RuntimeError ( f ' Target is not an ordinary directory: { raw } ' )
target = safe_child ( ROOT , raw )
if target . parent != ROOT or target . name != name :
raise RuntimeError ( f ' Target must be a direct child of the primary repository: { target } ' )
if git ( ' check-ignore ' , ' -q ' , ' -- ' , name + ' /Assets ' ) . returncode != 0 :
raise RuntimeError ( f ' Add / { name } / to the primary .gitignore before initialization. ' )
if git_text ( ' ls-files ' , ' -- ' , name ) :
raise RuntimeError ( f ' Target already contains tracked files: { name } ' )
targets [ name ] = target
initialized = datetime . now ( HK ) . isoformat ( timespec = ' seconds ' )
run_id = datetime . now ( HK ) . strftime ( ' % Y % m %d T % H % M % S ' ) + ' - ' + uuid . uuid4 ( ) . hex [ : 8 ]
logs = safe_child ( ROOT , ROOT / ' Logs/WorkspaceInitialization ' )
run = safe_child ( logs , logs / run_id )
run . mkdir ( parents = True )
files = baseline_files ( commit , run )
required_bytes = sum ( map ( len , files . values ( ) ) ) * ( sum ( not p . exists ( ) for p in targets . values ( ) ) + 1 ) + 64 * 1024 * 1024
free_bytes = shutil . disk_usage ( ROOT ) . free
if free_bytes < required_bytes :
raise RuntimeError ( f ' Not enough free disk space: { free_bytes } < { required_bytes } ' )
prepared = { }
outcomes = { }
# All three copies are validated before publishing any new directory.
for name , destination in targets . items ( ) :
if destination . exists ( ) :
outcomes [ name ] = ' verified-existing '
validate_copy ( destination , destination , name , files , commit )
prepared [ name ] = destination
else :
staging = safe_child ( run , run / name )
create_copy ( staging , destination , name , files , commit , initialized )
validate_copy ( staging , destination , name , files , commit )
prepared [ name ] = staging
outcomes [ name ] = ' created '
results = [ ]
for name , destination in targets . items ( ) :
source = prepared [ name ]
if source != destination :
# Recheck resolved absolute source and destination immediately before moving.
if safe_child ( run , source ) != source or safe_child ( ROOT , destination ) != destination :
raise RuntimeError ( ' Publication paths changed. ' )
if destination . exists ( ) or destination . is_symlink ( ) :
raise RuntimeError ( f ' Target appeared during preparation; refusing to overwrite: { destination } ' )
source . rename ( destination )
result = validate_copy ( destination , destination , name , files , commit )
result [ ' operation ' ] = outcomes [ name ]
if git ( ' check-ignore ' , ' -q ' , ' -- ' , name + ' /WorkspaceInfo.json ' ) . returncode != 0 :
raise RuntimeError ( ' Published copy is not ignored by the primary repository. ' )
results . append ( result )
if git_state ( ) != state_before :
raise RuntimeError ( ' Primary HEAD, branches, worktrees or index changed during initialization. ' )
if main_assets_state ( ) != assets_before :
raise RuntimeError ( ' Primary Assets changed during initialization. ' )
report = {
' schemaVersion ' : 1 , ' initializedAt ' : initialized , ' runId ' : run_id ,
' sourceRepository ' : ROOT . as_posix ( ) , ' sourceCommit ' : commit ,
' baselineFileCount ' : len ( files ) , ' baselineBytes ' : sum ( map ( len , files . values ( ) ) ) ,
' freeDiskBytesAtStart ' : free_bytes , ' trees ' : results ,
' primaryAssetsUnchanged ' : True , ' gitHeadUnchanged ' : True , ' gitBranchesUnchanged ' : True ,
' gitWorktreeRegistrationsUnchanged ' : True , ' gitIndexUnchanged ' : True ,
' primaryGitState ' : state_before ,
' scope ' : ' Static resource initialization only; Unity/Blender/MCP were not started. ' ,
' pending ' : [ ' Unity package resolution/import/compilation ' , ' MCP explicit instance binding ' ,
' Separate test saves and PlayerPrefs before parallel play ' , ' Branches/worktrees only when requested ' ] ,
}
write_json ( run / ' Result.json ' , report )
qa = safe_child ( ROOT , ROOT / ' Docs/QA/Workspaces ' )
qa . mkdir ( parents = True , exist_ok = True )
write_json ( qa / ' Initialization.json ' , report )
rows = ' \n ' . join ( f " | { r [ ' name ' ] } | { r [ ' totalFiles ' ] } | { r [ ' bytes ' ] / 1048576 : .2f } MiB | { r [ ' unityMetaFiles ' ] } | { r [ ' operation ' ] } / passed | " for r in results )
( qa / ' Initialization.md ' ) . write_text (
' # tree1– tree3 资源初始化验收 \n \n '
f ' 时间: { initialized } ( Asia/Hong_Kong)。基线:`{ commit } `。 \n \n '
' | 目录 | 文件数 | 大小 | Unity .meta | 结果 | \n | --- | --- | --- | --- | --- | \n ' + rows + ' \n \n '
f ' 每份包含 { len ( files ) } 个基线文件、4 份工具路径适配及 3 份工作区说明/清单文件。 '
f ' 其中 { len ( files ) - 4 } 个基线文件逐字节保留;适配文件与源、目标路径均按清单验证。 \n \n '
' 检查通过:逐文件 SHA-256、资产与 .meta 配对、GUID 唯一且与基线一致、 '
' 中英文表键一致、源模型/字体/许可/包锁完整、无共享可写链接、无缓存或构建包、 '
' 父仓库忽略规则生效。主工程 Assets、HEAD、分支、worktree 注册及 Git 暂存区均保持原状。 \n \n '
' 三个目录均为普通资源副本,没有独立 Git 历史。MCP 实例未连接; '
' Unity 导入、编译、播放和构建未执行;并行运行前仍需隔离存档及 PlayerPrefs。 \n \n '
' 完整记录见 [Initialization.json](Initialization.json),目录内的 InitializationManifest.json '
' 提供每个基线文件与适配项的哈希。执行计划见 [TreeInitializationPlan.md](../../Production/TreeInitializationPlan.md)。 \n ' ,
encoding = ' utf-8 ' )
print ( json . dumps ( { key : report [ key ] for key in ( ' runId ' , ' sourceCommit ' , ' baselineFileCount ' , ' trees ' , ' primaryAssetsUnchanged ' , ' gitBranchesUnchanged ' , ' gitWorktreeRegistrationsUnchanged ' ) } , ensure_ascii = False , indent = 2 ) )
if __name__ == ' __main__ ' :
main ( )