fix(report): make density facts readable and printable
Unify reader cleanup rules, lock writer table guards, and register the exact fictional timestamp collision. Preserve existing ordinary-report safety contracts and source-data gaps. Validation: report Node 165/165, final safety 29/29, Python 101/101, Chrome 28/28; both PDFs retain all 130 rows. Full Node 3704 tests with the same 91 baseline failures. Privacy test: 62 passed, 1 failed due to 17 protected-file READ_ERRORs; not a green gate. Build, DB, manual checklist and controlled-login gaps remain documented. User explicitly authorized staging push with these gaps disclosed. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -2,29 +2,30 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
STATUS_LABELS = {
|
||||
"parameter_sensitive": "参数敏感",
|
||||
"pyjhora_behavior_only": "仅单一外部参照,未做多引擎核对",
|
||||
"not_multiengine_parity": "仅单一外部参照,未做多引擎核对",
|
||||
"unresolved_external_tuple_boundary": "外部边界未对齐",
|
||||
"partial_verified": "部分核验",
|
||||
"raw_appendix_only": "仅原始附录可见",
|
||||
"missing_in_local": "本地暂无",
|
||||
"internal_reference_omitted": "内部参照已省略",
|
||||
}
|
||||
CELL_LABELS = {"blocked": "暂不可用", "executed": "已执行", "available": "可用", "computed": "已计算"}
|
||||
_TOKEN = re.compile(r"\b(" + "|".join(STATUS_LABELS) + r")\b")
|
||||
_PRODUCT = re.compile(
|
||||
r"\bPL9(?:\.pdf)?(?:[ \t]*(?:第[ \t]*\d+(?:[ \t]*[–—/-][ \t]*\d+)*[ \t]*页|"
|
||||
r"(?:pages?|p)[ \t-]*\d+(?:[ \t]*[–—/-][ \t]*\d+)*))?\b|"
|
||||
r"\bPL9[ \t]*第[ \t]*\d+(?:[ \t]*[–—/-][ \t]*\d+)*[ \t]*页",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_FUNCTION = re.compile(r"\b(?:jyotish_engine\.)?(?:cmd_[a-z0-9_]+|render_pl9_markdown|build_professional_report_reference_packet)\b")
|
||||
_CELL = re.compile(r"(?<=\|)([ \t]*)(`?)(blocked|executed|available|computed)\2([ \t]*)(?=\|)")
|
||||
# Both runtimes consume this file. ASCII regex semantics deliberately match JS:
|
||||
# Python's default Unicode \b would leave Chinese page references untranslated.
|
||||
_RULES = json.loads(Path(__file__).with_suffix(".rules.json").read_text(encoding="utf-8"))
|
||||
_CHART_FENCE = re.compile(_RULES["chartFencePattern"], re.MULTILINE | re.ASCII)
|
||||
_REPLACEMENTS = [
|
||||
(re.compile(rule["pattern"], re.ASCII | (re.IGNORECASE if rule.get("ignoreCase") else 0)), rule)
|
||||
for rule in _RULES["rules"]
|
||||
]
|
||||
|
||||
|
||||
def _replace(match: re.Match[str], rule: dict) -> str:
|
||||
if "replacement" in rule:
|
||||
return rule["replacement"]
|
||||
return (
|
||||
rule.get("prefix", "")
|
||||
+ "".join(match[group] for group in rule.get("beforeGroups", []))
|
||||
+ rule["labels"][match[rule["labelGroup"]]]
|
||||
+ "".join(match[group] for group in rule.get("afterGroups", []))
|
||||
)
|
||||
|
||||
|
||||
def clean_reader_appendix_markdown(markdown: str) -> str:
|
||||
@@ -33,13 +34,10 @@ def clean_reader_appendix_markdown(markdown: str) -> str:
|
||||
Ambiguous English words are changed only as complete pipe-table cells.
|
||||
Chart fences are immutable: their JSON is a separate renderer contract.
|
||||
"""
|
||||
parts = re.split(r"(^[ \t]*```jyotish-chart[^\n]*\n[\s\S]*?^[ \t]*```[^\n]*(?:\n|$))", markdown, flags=re.MULTILINE)
|
||||
parts = _CHART_FENCE.split(markdown)
|
||||
for index in range(0, len(parts), 2):
|
||||
value = _TOKEN.sub(lambda match: STATUS_LABELS[match[0]], parts[index])
|
||||
value = _FUNCTION.sub("本地计算", value)
|
||||
value = re.sub(r"\b(?:[a-z0-9_]+pl9[a-z0-9_]*|pl9_[a-z0-9_]+)(?:\.v\d+)?\b", "外部参照资料", value, flags=re.IGNORECASE)
|
||||
value = _PRODUCT.sub("外部参照资料", value)
|
||||
value = re.sub(r"\b(?:PyJHora|JHora)\b", "外部参照引擎", value, flags=re.IGNORECASE)
|
||||
value = _CELL.sub(lambda match: f"{match[1]}{match[2]}{CELL_LABELS[match[3]]}{match[2]}{match[4]}", value)
|
||||
value = parts[index]
|
||||
for pattern, rule in _REPLACEMENTS:
|
||||
value = pattern.sub(lambda match: _replace(match, rule), value)
|
||||
parts[index] = value
|
||||
return "".join(parts)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"chartFencePattern": "(^[ \\t]*```jyotish-chart[^\\n]*\\n[\\s\\S]*?^[ \\t]*```[^\\n]*(?:\\n|$))",
|
||||
"rules": [
|
||||
{
|
||||
"pattern": "\\b(parameter_sensitive|pyjhora_behavior_only|not_multiengine_parity|unresolved_external_tuple_boundary|partial_verified|raw_appendix_only|missing_in_local|internal_reference_omitted)\\b",
|
||||
"labels": {
|
||||
"parameter_sensitive": "参数敏感",
|
||||
"pyjhora_behavior_only": "仅单一外部参照,未做多引擎核对",
|
||||
"not_multiengine_parity": "仅单一外部参照,未做多引擎核对",
|
||||
"unresolved_external_tuple_boundary": "外部边界未对齐",
|
||||
"partial_verified": "部分核验",
|
||||
"raw_appendix_only": "仅原始附录可见",
|
||||
"missing_in_local": "本地暂无",
|
||||
"internal_reference_omitted": "内部参照已省略"
|
||||
},
|
||||
"labelGroup": 0
|
||||
},
|
||||
{
|
||||
"pattern": "\\b(?:jyotish_engine\\.)?(?:cmd_[a-z0-9_]+|render_pl9_markdown|build_professional_report_reference_packet)\\b",
|
||||
"replacement": "本地计算"
|
||||
},
|
||||
{
|
||||
"pattern": "\\b(?:[a-z0-9_]+pl9[a-z0-9_]*|pl9_[a-z0-9_]+)(?:\\.v\\d+)?\\b",
|
||||
"ignoreCase": true,
|
||||
"replacement": "外部参照资料"
|
||||
},
|
||||
{
|
||||
"pattern": "\\bPL9\\b(?:\\.pdf)?(?:[ \\t]*(?:第[ \\t]*\\d+(?:[ \\t]*[–—/-][ \\t]*\\d+)*[ \\t]*页|(?:pages?|p)[ \\t-]*\\d+(?:[ \\t]*[–—/-][ \\t]*\\d+)*))?",
|
||||
"ignoreCase": true,
|
||||
"replacement": "外部参照资料"
|
||||
},
|
||||
{
|
||||
"pattern": "\\b(?:PyJHora|JHora)\\b",
|
||||
"ignoreCase": true,
|
||||
"replacement": "外部参照引擎"
|
||||
},
|
||||
{
|
||||
"pattern": "\\|([ \\t]*)(`?)(blocked|executed|available|computed)\\2([ \\t]*)(?=\\|)",
|
||||
"labels": { "blocked": "暂不可用", "executed": "已执行", "available": "可用", "computed": "已计算" },
|
||||
"labelGroup": 3,
|
||||
"prefix": "|",
|
||||
"beforeGroups": [1, 2],
|
||||
"afterGroups": [2, 4]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,42 +12,31 @@ def validate_fact_tables(value, add):
|
||||
def text(item, maximum, minimum=0):
|
||||
return isinstance(item, str) and minimum <= len(item) <= maximum
|
||||
|
||||
if not isinstance(value, list) or len(value) > 8:
|
||||
add("factTables", "must be array with at most eight groups")
|
||||
return
|
||||
seen = set()
|
||||
for index, table in enumerate(value):
|
||||
path = f"factTables[{index}]"
|
||||
if not isinstance(table, dict) or set(table) != {"id", "title", "claimStatus", "sourcePath", "note", "columns", "rows"}:
|
||||
add(path, "invalid table keys")
|
||||
continue
|
||||
group = table["id"]
|
||||
if not isinstance(group, str) or group not in GROUPS or group in seen:
|
||||
add(path, "invalid or duplicate group")
|
||||
else:
|
||||
seen.add(group)
|
||||
source = table["sourcePath"]
|
||||
def within(child, parent):
|
||||
return isinstance(child, str) and bool(PATH.fullmatch(child)) and (child == parent or child.startswith(parent + ".") or child.startswith(parent + "["))
|
||||
|
||||
def grid(table, path, parent=None):
|
||||
source = table.get("sourcePath")
|
||||
if not isinstance(source, str) or not PATH.fullmatch(source):
|
||||
add(path, "invalid source path")
|
||||
continue
|
||||
if not text(table["title"], 120, 1) or not text(table["note"], 500):
|
||||
return
|
||||
if parent is not None and not within(source, parent):
|
||||
add(path, "fact_table_source_mismatch")
|
||||
if not text(table.get("title"), 120, 1) or not text(table.get("note"), 500):
|
||||
add(path, "invalid text")
|
||||
if table["claimStatus"] not in STATUSES:
|
||||
add(path, "invalid claim status")
|
||||
columns = table["columns"]
|
||||
columns = table.get("columns")
|
||||
if not isinstance(columns, list) or not 1 <= len(columns) <= 16 or not all(text(c, 80, 1) for c in columns):
|
||||
add(path, "invalid columns")
|
||||
continue
|
||||
rows = table["rows"]
|
||||
return
|
||||
rows = table.get("rows")
|
||||
if not isinstance(rows, list) or len(rows) > 2000:
|
||||
add(path, "invalid rows")
|
||||
continue
|
||||
return
|
||||
for row in rows:
|
||||
if not isinstance(row, dict) or set(row) != {"sourcePath", "cells"}:
|
||||
add(path, "invalid row keys")
|
||||
continue
|
||||
origin = row["sourcePath"]
|
||||
if not isinstance(origin, str) or not PATH.fullmatch(origin) or not (origin == source or origin.startswith(source + ".") or origin.startswith(source + "[")):
|
||||
if not within(row["sourcePath"], source):
|
||||
add(path, "fact_table_source_mismatch")
|
||||
cells = row["cells"]
|
||||
if not isinstance(cells, list) or len(cells) != len(columns):
|
||||
@@ -59,3 +48,43 @@ def validate_fact_tables(value, add):
|
||||
if isinstance(cell, (int, float)) and math.isfinite(cell):
|
||||
continue
|
||||
add(path, "invalid cell")
|
||||
|
||||
if not isinstance(value, list) or len(value) > 8:
|
||||
add("factTables", "must be array with at most eight groups")
|
||||
return
|
||||
seen = set()
|
||||
required = {"id", "title", "claimStatus", "sourcePath", "note", "columns", "rows"}
|
||||
subkeys = required - {"claimStatus"}
|
||||
for index, table in enumerate(value):
|
||||
path = f"factTables[{index}]"
|
||||
if not isinstance(table, dict) or not required <= set(table) or set(table) - required - {"subtables"}:
|
||||
add(path, "invalid table keys")
|
||||
continue
|
||||
group = table["id"]
|
||||
if not isinstance(group, str) or group not in GROUPS or group in seen:
|
||||
add(path, "invalid or duplicate group")
|
||||
else:
|
||||
seen.add(group)
|
||||
if not isinstance(table["claimStatus"], str) or table["claimStatus"] not in STATUSES:
|
||||
add(path, "invalid claim status")
|
||||
grid(table, path)
|
||||
if "subtables" not in table:
|
||||
continue
|
||||
children = table["subtables"]
|
||||
if not isinstance(children, list) or len(children) > 8:
|
||||
add(path, "invalid subtables")
|
||||
continue
|
||||
child_ids = set()
|
||||
for child_index, child in enumerate(children):
|
||||
child_path = f"{path}.subtables[{child_index}]"
|
||||
if not isinstance(child, dict) or set(child) != subkeys:
|
||||
add(child_path, "invalid subtable keys")
|
||||
continue
|
||||
child_id = child["id"]
|
||||
if not text(child_id, 80, 1) or not re.fullmatch(r"[a-z][a-z0-9_-]*", child_id) or child_id in child_ids:
|
||||
add(child_path, "invalid or duplicate subtable")
|
||||
else:
|
||||
child_ids.add(child_id)
|
||||
parent = table["sourcePath"]
|
||||
if isinstance(parent, str):
|
||||
grid(child, child_path, parent)
|
||||
|
||||
Reference in New Issue
Block a user