tools/epro2/std: add Pro 2.x JSON path — Liangshan + Taishan SCH now exportable

The downstream colleague's "encrypted_external" / "string old format"
projects were Pro 2.x, not Pro 3.x EPRO2. Pro 2.x ships each doc as a
JSON file whose `dataStr` is a plaintext op-stream — one JSON array per
line, e.g. `["COMPONENT","e1","",0,0,0,0,{},0]`. Different wire format
from EPRO2's binary tilde/pipe streams; same Std envelope works for
output.

  - tools/epro2/std/pro2_writer.py: parses dataStr line-by-line, keys
    objects by id (position 1 for most ops, OPTYPE for singletons),
    extracts BBox by walking known coord positions per OPTYPE, derives
    layers from LAYER ops directly (Pro 2.x almost matches Std layer
    string format already). PCB blobs that are encrypted-external
    (`dataStrId` URL + `iv` + `key`, no inline dataStr — Taishan PCB)
    return None so the CLI skips with a message instead of stubbing.

  - tools/epro2/std/__main__.py: auto-detect via manifest's
    editor_version. "2.x" → Pro 2.x writer; otherwise the existing
    EPRO2 replay path. CLI surface and output layout unchanged.

  - docs/sources/epro2_to_std_mapping.md: adds a Pro 2.x section.
    Adapter dispatches on `head.epro_format`: absent / "epro2" gets
    dict-shaped objects values, "pro2" gets array-shaped values
    (`[OPTYPE, arg1, ...]`). Lists the Pro 2.x-specific OPTYPEs
    (FONTSTYLE / LINESTYLE / CONNECT / OBJ / REGION / DIMENSION /
    STRING / TEARDROP) the EPRO2 vocabulary doesn't have.

Smoke (re-running --all on all 5 Pro projects): 191 → 222 JSON files.
Liangshan adds 3 (2 SCH + inline 5357-object PCB). Taishan adds 28
(SCH only — PCB skipped, encrypted-external; source/<uuid>.json still
keeps the dataStrId/iv/key for a later fetch+decrypt pass).

84 → 86 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-29 02:00:37 +08:00
parent 3866e24189
commit 3720cd176a
4 changed files with 440 additions and 3 deletions

View File

@@ -155,6 +155,69 @@ def test_sch_objects_dict_preserved():
# -- json round-trip ---------------------------------------------------
def test_pro2_parses_inline_datastr_into_objects_dict():
"""Pro 2.x docs store dataStr as a plaintext op-stream (one JSON
array per line). Each id-keyed op (most of them — COMPONENT / ATTR /
LINE / WIRE / VIA / ...) lands in objects[id] as the raw array.
Singletons (DOCTYPE / HEAD / CANVAS / ...) get keyed by their
OPTYPE name. This is what downstream's adapter receives and
dispatches on."""
import tempfile
from pathlib import Path
from tools.epro2.std.pro2_writer import write_pro2_doc
json_text = json.dumps({
"uuid": "doc-1",
"title": "test sch",
"docType": 1,
"dataStr": (
'["DOCTYPE","SCH","1.1"]\n'
'["HEAD",{"originX":0,"originY":0,"version":"2.1.39","maxId":42}]\n'
'["COMPONENT","e1","",0,0,90,0,{},0]\n'
'["WIRE","e2",100,200,300,200]\n'
'["LINE","e3",1,500,600,700,600,6,"st1",0]\n'
),
})
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
f.write(json_text)
tmp = Path(f.name)
payload = write_pro2_doc(tmp, project_uuid="proj-1")
objs = payload["result"]["dataStr"]["objects"]
# singletons keyed by OPTYPE
assert objs["DOCTYPE"] == ["DOCTYPE", "SCH", "1.1"]
assert objs["HEAD"][0] == "HEAD"
# id-keyed by position-1 string
assert objs["e1"] == ["COMPONENT", "e1", "", 0, 0, 90, 0, {}, 0]
assert objs["e2"] == ["WIRE", "e2", 100, 200, 300, 200]
# head propagates the EPRO2 format hint so adapter knows which dispatch
head = payload["result"]["dataStr"]["head"]
assert head["epro_format"] == "pro2"
assert head["units"] == "mil"
assert "2.1.39" in head["editorVersion"]
def test_pro2_skips_encrypted_external_pcb():
"""Pro 2.x PCB docs sometimes store the dataStr at modules.lceda.cn
keyed by `dataStrId` + AES-decrypted with `iv`/`key`. We don't fetch
those — return None so the CLI can skip with a stats bump rather
than emit a stub JSON the downstream parser can't make sense of."""
import tempfile
from pathlib import Path
from tools.epro2.std.pro2_writer import write_pro2_doc
enc = json.dumps({
"uuid": "doc-pcb",
"docType": 3,
"dataStrId": "https://modules.lceda.cn/datastr/abc...",
"iv": "abcd1234abcd1234abcd1234",
"key": "deadbeef" * 8,
})
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
f.write(enc)
tmp = Path(f.name)
assert write_pro2_doc(tmp) is None
def test_writers_round_trip_through_json_dump():
"""Our payloads must survive json.dumps without TypeError — catches
Decimal / datetime / bytes leaks early."""