183 lines
5.1 KiB
Python
183 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
DIRECTORIES = (
|
|
"backend",
|
|
"config",
|
|
"static",
|
|
"tests",
|
|
"tools",
|
|
"vendor",
|
|
"游资skills",
|
|
)
|
|
|
|
ROOT_FILES = (
|
|
".dockerignore",
|
|
".env.example",
|
|
".gitignore",
|
|
"advanced_strategies.py",
|
|
"alert_service.py",
|
|
"api_access.py",
|
|
"app_config.py",
|
|
"ARCHITECTURE.md",
|
|
"assistant_agent.py",
|
|
"chart_data_provider.py",
|
|
"compose.yaml",
|
|
"database.py",
|
|
"demo_data.py",
|
|
"Dockerfile",
|
|
"DOCKER_DEPLOY.md",
|
|
"heaven_agent.py",
|
|
"heaven_engine.py",
|
|
"ifind_client.py",
|
|
"llm_strategy.py",
|
|
"llm_stream.py",
|
|
"market_insights.py",
|
|
"mentor_agent.py",
|
|
"package-lock.json",
|
|
"package.json",
|
|
"playwright.config.js",
|
|
"README.md",
|
|
"realtime_aggregator.py",
|
|
"requirements.txt",
|
|
"screener.py",
|
|
"security.py",
|
|
"sentiment_engine.py",
|
|
"server.py",
|
|
"strategy_tracking.py",
|
|
"sync_data.py",
|
|
"THIRD_PARTY_NOTICES.md",
|
|
"trade_journal.py",
|
|
"tushare_client.py",
|
|
)
|
|
|
|
|
|
def digest(path: Path) -> str:
|
|
checksum = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
|
checksum.update(block)
|
|
return checksum.hexdigest()
|
|
|
|
|
|
def display_path(path: Path, base: Path) -> str:
|
|
try:
|
|
return path.relative_to(base).as_posix() or "."
|
|
except ValueError:
|
|
return path.as_posix()
|
|
|
|
|
|
def source_files(source_root: Path) -> list[Path]:
|
|
files = [source_root / name for name in ROOT_FILES]
|
|
for directory in DIRECTORIES:
|
|
root = source_root / directory
|
|
if not root.is_dir():
|
|
continue
|
|
files.extend(
|
|
path
|
|
for path in root.rglob("*")
|
|
if path.is_file() and "__pycache__" not in path.parts
|
|
)
|
|
return sorted(
|
|
set(files), key=lambda path: path.relative_to(source_root).as_posix()
|
|
)
|
|
|
|
|
|
def build_manifest(
|
|
source_root: Path,
|
|
target_root: Path,
|
|
source_commit: str,
|
|
) -> dict[str, object]:
|
|
assets = []
|
|
mismatches = []
|
|
for source in source_files(source_root):
|
|
relative = source.relative_to(source_root)
|
|
target = target_root / relative
|
|
source_exists = source.is_file()
|
|
source_hash = digest(source) if source_exists else ""
|
|
target_hash = digest(target) if target.is_file() else ""
|
|
if not source_exists:
|
|
status = "missing_source"
|
|
elif not target.is_file():
|
|
status = "missing_target"
|
|
elif source_hash == target_hash:
|
|
status = "identical"
|
|
else:
|
|
status = "mismatch"
|
|
if status != "identical":
|
|
mismatches.append(relative.as_posix())
|
|
assets.append(
|
|
{
|
|
"source": relative.as_posix(),
|
|
"target": display_path(target, REPOSITORY_ROOT),
|
|
"bytes": source.stat().st_size if source_exists else 0,
|
|
"sha256": source_hash,
|
|
"disposition": "original_copy_pending_move",
|
|
"status": status,
|
|
}
|
|
)
|
|
return {
|
|
"schema_version": 1,
|
|
"generated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
|
"source_commit": source_commit,
|
|
"source_root": display_path(source_root, REPOSITORY_ROOT),
|
|
"target_root": display_path(target_root, REPOSITORY_ROOT),
|
|
"excluded": [
|
|
"next/",
|
|
"data/review.db and SQLite sidecars",
|
|
"data/private-mentor-skills/",
|
|
".env",
|
|
"node_modules/",
|
|
"logs, caches and generated test results",
|
|
],
|
|
"asset_count": len(assets),
|
|
"mismatch_count": len(mismatches),
|
|
"mismatches": mismatches,
|
|
"assets": assets,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Build an exact-copy manifest for a preservation migration stage. "
|
|
"This is a migration-only tool; it does not describe the final moved layout."
|
|
)
|
|
)
|
|
parser.add_argument("--source-root", type=Path, default=REPOSITORY_ROOT)
|
|
parser.add_argument("--target-root", type=Path, default=REPOSITORY_ROOT / "app")
|
|
parser.add_argument("--source-commit", default="")
|
|
parser.add_argument(
|
|
"--output",
|
|
type=Path,
|
|
required=True,
|
|
help="write to a new audit path; do not overwrite committed slice evidence",
|
|
)
|
|
args = parser.parse_args()
|
|
source_root = args.source_root.resolve()
|
|
target_root = args.target_root.resolve()
|
|
output = args.output.resolve()
|
|
manifest = build_manifest(source_root, target_root, args.source_commit)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
output.write_text(
|
|
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(
|
|
f"assets={manifest['asset_count']} mismatches={manifest['mismatch_count']} "
|
|
f"output={output}"
|
|
)
|
|
return 1 if manifest["mismatch_count"] else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|