cadence-diff 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cadence_diff-0.1.0.dist-info/METADATA +245 -0
- cadence_diff-0.1.0.dist-info/RECORD +78 -0
- cadence_diff-0.1.0.dist-info/WHEEL +4 -0
- cadence_diff-0.1.0.dist-info/entry_points.txt +3 -0
- cadence_diff-0.1.0.dist-info/licenses/LICENSE +190 -0
- qc_tool/__init__.py +3 -0
- qc_tool/__main__.py +6 -0
- qc_tool/attestation.py +209 -0
- qc_tool/availability.py +273 -0
- qc_tool/cli.py +769 -0
- qc_tool/config/__init__.py +1 -0
- qc_tool/config/lint.py +480 -0
- qc_tool/config/profile.py +207 -0
- qc_tool/coverage.py +35 -0
- qc_tool/crosscheck/__init__.py +1 -0
- qc_tool/crosscheck/numbers.py +146 -0
- qc_tool/crosscheck/package.py +154 -0
- qc_tool/crosscheck/trace.py +374 -0
- qc_tool/engine.py +978 -0
- qc_tool/excel/__init__.py +1 -0
- qc_tool/excel/align.py +452 -0
- qc_tool/excel/charts.py +931 -0
- qc_tool/excel/context.py +106 -0
- qc_tool/excel/controls.py +266 -0
- qc_tool/excel/dependency.py +252 -0
- qc_tool/excel/diff_structure.py +326 -0
- qc_tool/excel/diff_values.py +327 -0
- qc_tool/excel/formulas.py +516 -0
- qc_tool/excel/interaction.py +548 -0
- qc_tool/excel/periods.py +117 -0
- qc_tool/excel/preflight.py +615 -0
- qc_tool/excel/references.py +436 -0
- qc_tool/excel/regions.py +201 -0
- qc_tool/findings.py +220 -0
- qc_tool/fingerprint.py +369 -0
- qc_tool/fingerprint.schema.json +15 -0
- qc_tool/history/__init__.py +1 -0
- qc_tool/history/store.py +287 -0
- qc_tool/io/__init__.py +1 -0
- qc_tool/io/decrypt.py +64 -0
- qc_tool/io/excel_formula.py +256 -0
- qc_tool/io/excel_formula_worker.py +280 -0
- qc_tool/io/formula_enrichment.py +95 -0
- qc_tool/io/libreoffice_formula.py +232 -0
- qc_tool/io/loader.py +973 -0
- qc_tool/io/model.py +356 -0
- qc_tool/io/ooxml_chart.py +530 -0
- qc_tool/io/ooxml_interaction.py +400 -0
- qc_tool/io/ooxml_worksheet.py +405 -0
- qc_tool/io/xlsb_formula.py +313 -0
- qc_tool/package-sanitize.schema.json +31 -0
- qc_tool/package_sanitize.py +255 -0
- qc_tool/ppt/__init__.py +1 -0
- qc_tool/ppt/chart_xml.py +422 -0
- qc_tool/ppt/diff.py +120 -0
- qc_tool/ppt/element_diff.py +878 -0
- qc_tool/ppt/element_match.py +317 -0
- qc_tool/ppt/extract.py +212 -0
- qc_tool/ppt/match.py +137 -0
- qc_tool/ppt/model.py +132 -0
- qc_tool/ppt/preflight.py +307 -0
- qc_tool/privacy.py +392 -0
- qc_tool/progress.py +78 -0
- qc_tool/report/__init__.py +1 -0
- qc_tool/report/excel_report.py +205 -0
- qc_tool/report/findings.schema.json +32 -0
- qc_tool/report/html_report.py +38 -0
- qc_tool/report/json_report.py +55 -0
- qc_tool/report/templates/report.html.j2 +117 -0
- qc_tool/sanitize.py +598 -0
- qc_tool/security.py +34 -0
- qc_tool/server_config.py +86 -0
- qc_tool/triage/__init__.py +1 -0
- qc_tool/triage/rules.py +211 -0
- qc_tool/ui/__init__.py +1 -0
- qc_tool/ui/app.py +1261 -0
- qc_tool/ui/guide.py +452 -0
- qc_tool/ui/theme.py +505 -0
qc_tool/coverage.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Explicit run-mode and check-coverage contracts shared by every surface."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class QCRunMode(StrEnum):
|
|
9
|
+
CYCLE_COMPARISON = "cycle_comparison"
|
|
10
|
+
CURRENT_FILE_PREFLIGHT = "current_file_preflight"
|
|
11
|
+
FINAL_PACKAGE = "final_package"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CoverageState(StrEnum):
|
|
15
|
+
CHECKED = "checked"
|
|
16
|
+
UNAVAILABLE = "unavailable"
|
|
17
|
+
DEGRADED = "degraded"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CoverageItem(BaseModel):
|
|
21
|
+
check_id: str
|
|
22
|
+
label: str
|
|
23
|
+
artifact: str
|
|
24
|
+
state: CoverageState
|
|
25
|
+
findings: int = 0
|
|
26
|
+
detail: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MappingCoverage(BaseModel):
|
|
30
|
+
eligible: int = 0
|
|
31
|
+
mapped: int = 0
|
|
32
|
+
verified: int = 0
|
|
33
|
+
mismatched: int = 0
|
|
34
|
+
unresolved: int = 0
|
|
35
|
+
unmapped: int = 0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Excel-to-PPT figure cross-checking: format-aware matching and source tracing."""
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Format-aware figure parsing and display-precision matching.
|
|
2
|
+
|
|
3
|
+
Deck figures are rounded, scaled, and decorated ("$1.2M", "12%",
|
|
4
|
+
"(1,234)"). A figure matches a workbook value when the value, projected
|
|
5
|
+
into the figure's display space (scale applied, percent multiplied out),
|
|
6
|
+
falls within half a unit of the figure's last displayed decimal — robust
|
|
7
|
+
to the different rounding modes of Excel and Python.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
_SUFFIX_SCALE = {
|
|
14
|
+
"k": 1e3,
|
|
15
|
+
"m": 1e6,
|
|
16
|
+
"mn": 1e6,
|
|
17
|
+
"b": 1e9,
|
|
18
|
+
"bn": 1e9,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_FIGURE_RE = re.compile(
|
|
22
|
+
r"(?<![\w.,-])" # not glued to a word, number, or hyphenated label (Jan-26)
|
|
23
|
+
r"(?P<open>\()?\s*"
|
|
24
|
+
r"(?P<currency>[$\u20ac\u00a3\u20b9])?\s*"
|
|
25
|
+
r"(?P<sign>-)?"
|
|
26
|
+
r"(?P<number>\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?)"
|
|
27
|
+
r"\s*(?P<suffix>%|bn|mn|k|m|b)?"
|
|
28
|
+
r"(?P<close>\))?"
|
|
29
|
+
r"(?![\w%])",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
#: Numeric tokens blanked out — used for line skeletons that stay stable
|
|
34
|
+
#: while figures refresh each cycle.
|
|
35
|
+
_NUMERIC_TOKEN_RE = re.compile(r"[-+]?[\d.,]+\s*%?")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def numeric_skeleton(text: str) -> str:
|
|
39
|
+
"""Text with numeric tokens blanked; equal skeletons = same wording."""
|
|
40
|
+
return _NUMERIC_TOKEN_RE.sub("#", text)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class ParsedFigure:
|
|
45
|
+
raw: str
|
|
46
|
+
mantissa: float # the number as displayed (1.2 for "$1.2M")
|
|
47
|
+
scale: float # 1e6 for M, 1e3 for k, 1 otherwise
|
|
48
|
+
is_percent: bool
|
|
49
|
+
decimals: int # displayed decimal places of the mantissa
|
|
50
|
+
negative: bool
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def value(self) -> float:
|
|
54
|
+
"""Normalized numeric value ($1.2M -> 1_200_000; 12% -> 0.12)."""
|
|
55
|
+
signed = -self.mantissa if self.negative else self.mantissa
|
|
56
|
+
if self.is_percent:
|
|
57
|
+
return signed / 100.0
|
|
58
|
+
return signed * self.scale
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _from_match(match: re.Match[str]) -> ParsedFigure:
|
|
62
|
+
number = match["number"].replace(",", "")
|
|
63
|
+
mantissa = float(number)
|
|
64
|
+
decimals = len(number.rsplit(".", 1)[1]) if "." in number else 0
|
|
65
|
+
suffix = (match["suffix"] or "").lower()
|
|
66
|
+
negative = bool(match["sign"]) or bool(match["open"] and match["close"])
|
|
67
|
+
return ParsedFigure(
|
|
68
|
+
raw=match.group(0).strip(),
|
|
69
|
+
mantissa=mantissa,
|
|
70
|
+
scale=_SUFFIX_SCALE.get(suffix, 1.0),
|
|
71
|
+
is_percent=suffix == "%",
|
|
72
|
+
decimals=decimals,
|
|
73
|
+
negative=negative,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def parse_figure(text: str) -> ParsedFigure | None:
|
|
78
|
+
"""Parse a single figure token; None if the text is not one figure."""
|
|
79
|
+
match = _FIGURE_RE.search(text.strip())
|
|
80
|
+
if match is None or match.group(0).strip() != text.strip():
|
|
81
|
+
return None
|
|
82
|
+
return _from_match(match)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def extract_figures(text: str) -> list[ParsedFigure]:
|
|
86
|
+
"""All figure tokens in a line of display text."""
|
|
87
|
+
return [_from_match(m) for m in _FIGURE_RE.finditer(text)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def display_matches(figure: ParsedFigure, cell_value: float) -> bool:
|
|
91
|
+
"""True if ``cell_value`` displays as ``figure`` at its shown precision."""
|
|
92
|
+
scaled = cell_value * 100.0 if figure.is_percent else cell_value / figure.scale
|
|
93
|
+
shown = -figure.mantissa if figure.negative else figure.mantissa
|
|
94
|
+
tolerance = 0.5 * 10.0 ** (-figure.decimals) + 1e-9
|
|
95
|
+
return abs(scaled - shown) <= tolerance
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def relative_difference(figure: ParsedFigure, cell_value: float) -> float:
|
|
99
|
+
"""Relative distance between a figure and a value (for near-miss ranking)."""
|
|
100
|
+
reference = figure.value
|
|
101
|
+
if reference == 0:
|
|
102
|
+
return abs(cell_value)
|
|
103
|
+
return abs(cell_value - reference) / abs(reference)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def format_figure_like(figure: ParsedFigure, value: float) -> str:
|
|
107
|
+
"""Render ``value`` with the source figure's currency/scale/precision style."""
|
|
108
|
+
raw = figure.raw.strip()
|
|
109
|
+
currency_match = re.search(r"[$\u20ac\u00a3\u20b9]", raw)
|
|
110
|
+
currency = currency_match.group(0) if currency_match else ""
|
|
111
|
+
suffix_match = re.search(r"(%|bn|mn|k|m|b)\s*\)?$", raw, re.IGNORECASE)
|
|
112
|
+
suffix = suffix_match.group(1) if suffix_match else ""
|
|
113
|
+
scaled = value * 100.0 if figure.is_percent else value / figure.scale
|
|
114
|
+
negative = scaled < 0
|
|
115
|
+
magnitude = abs(scaled)
|
|
116
|
+
use_grouping = "," in raw
|
|
117
|
+
number = (
|
|
118
|
+
f"{magnitude:,.{figure.decimals}f}"
|
|
119
|
+
if use_grouping
|
|
120
|
+
else f"{magnitude:.{figure.decimals}f}"
|
|
121
|
+
)
|
|
122
|
+
rendered = f"{currency}{number}{suffix}"
|
|
123
|
+
if negative:
|
|
124
|
+
return f"({rendered})" if raw.startswith("(") else f"-{rendered}"
|
|
125
|
+
return rendered
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def replace_figure_ordinals(
|
|
129
|
+
text: str, replacements: dict[int, str], *, start_index: int = 0
|
|
130
|
+
) -> tuple[str, int]:
|
|
131
|
+
"""Replace global figure ordinals in one text fragment; return next index."""
|
|
132
|
+
index = start_index
|
|
133
|
+
|
|
134
|
+
def replace(match: re.Match[str]) -> str:
|
|
135
|
+
nonlocal index
|
|
136
|
+
current = index
|
|
137
|
+
index += 1
|
|
138
|
+
replacement = replacements.get(current)
|
|
139
|
+
if replacement is None:
|
|
140
|
+
return match.group(0)
|
|
141
|
+
raw = match.group(0)
|
|
142
|
+
leading = raw[: len(raw) - len(raw.lstrip())]
|
|
143
|
+
trailing = raw[len(raw.rstrip()) :]
|
|
144
|
+
return f"{leading}{replacement}{trailing}"
|
|
145
|
+
|
|
146
|
+
return _FIGURE_RE.sub(replace, text), index
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Current Excel plus current PowerPoint final-package reconciliation."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from qc_tool.config.profile import CrosscheckProfile
|
|
6
|
+
from qc_tool.coverage import CoverageItem, CoverageState, MappingCoverage
|
|
7
|
+
from qc_tool.crosscheck.trace import (
|
|
8
|
+
MappingSuggestion,
|
|
9
|
+
SuggestedSource,
|
|
10
|
+
extract_deck_figures,
|
|
11
|
+
mapping_identity,
|
|
12
|
+
occurrence_identity,
|
|
13
|
+
suggest_sources,
|
|
14
|
+
verify_mappings,
|
|
15
|
+
)
|
|
16
|
+
from qc_tool.excel.periods import Period, is_period_after, parse_period
|
|
17
|
+
from qc_tool.findings import Finding, FindingClass
|
|
18
|
+
from qc_tool.io.model import WorkbookSnapshot
|
|
19
|
+
from qc_tool.ppt.extract import DeckSnapshot
|
|
20
|
+
from qc_tool.ppt.preflight import contextual_periods
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(slots=True)
|
|
24
|
+
class PackageQCResult:
|
|
25
|
+
findings: list[Finding] = field(default_factory=list)
|
|
26
|
+
coverage: list[CoverageItem] = field(default_factory=list)
|
|
27
|
+
mapping_coverage: MappingCoverage = field(default_factory=MappingCoverage)
|
|
28
|
+
suggestions: list[MappingSuggestion] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _latest_workbook_periods(workbook: WorkbookSnapshot) -> dict[str, Period]:
|
|
32
|
+
latest: dict[str, Period] = {}
|
|
33
|
+
for sheet in workbook.sheets:
|
|
34
|
+
for cell in sheet.cells.values():
|
|
35
|
+
period = parse_period(cell.value)
|
|
36
|
+
if period is None:
|
|
37
|
+
continue
|
|
38
|
+
previous = latest.get(period.kind)
|
|
39
|
+
if previous is None or is_period_after(period, previous):
|
|
40
|
+
latest[period.kind] = period
|
|
41
|
+
return latest
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _period_reconciliation(
|
|
45
|
+
workbook: WorkbookSnapshot, deck: DeckSnapshot
|
|
46
|
+
) -> list[Finding]:
|
|
47
|
+
findings: list[Finding] = []
|
|
48
|
+
latest = _latest_workbook_periods(workbook)
|
|
49
|
+
for (kind, sort_key), sources in contextual_periods(deck).items():
|
|
50
|
+
workbook_period = latest.get(kind)
|
|
51
|
+
if workbook_period is None or workbook_period.sort_key == sort_key:
|
|
52
|
+
continue
|
|
53
|
+
findings.append(
|
|
54
|
+
Finding(
|
|
55
|
+
artifact="crosscheck",
|
|
56
|
+
finding_class=FindingClass.PACKAGE_PERIOD_MISMATCH,
|
|
57
|
+
element="reporting period",
|
|
58
|
+
baseline_value=workbook_period.label,
|
|
59
|
+
current_value="; ".join(sorted(sources)),
|
|
60
|
+
message=(
|
|
61
|
+
"PowerPoint contextual reporting period does not match the "
|
|
62
|
+
f"latest {kind} period in Excel ({workbook_period.label})"
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return findings
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def reconcile_package(
|
|
70
|
+
workbook: WorkbookSnapshot, deck: DeckSnapshot, profile: CrosscheckProfile
|
|
71
|
+
) -> PackageQCResult:
|
|
72
|
+
result = PackageQCResult()
|
|
73
|
+
occurrences = extract_deck_figures(deck)
|
|
74
|
+
mapped_identities = {mapping_identity(mapping) for mapping in profile.mappings}
|
|
75
|
+
mapped_occurrences = [
|
|
76
|
+
occurrence
|
|
77
|
+
for occurrence in occurrences
|
|
78
|
+
if occurrence_identity(occurrence) in mapped_identities
|
|
79
|
+
]
|
|
80
|
+
unmapped_occurrences = [
|
|
81
|
+
occurrence
|
|
82
|
+
for occurrence in occurrences
|
|
83
|
+
if occurrence_identity(occurrence) not in mapped_identities
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
verified = verify_mappings(deck, workbook, profile)
|
|
87
|
+
result.findings.extend(verified.findings)
|
|
88
|
+
mismatched = sum(
|
|
89
|
+
finding.finding_class is FindingClass.CROSSCHECK_MISMATCH
|
|
90
|
+
for finding in verified.findings
|
|
91
|
+
)
|
|
92
|
+
unresolved = sum(
|
|
93
|
+
finding.finding_class is FindingClass.CROSSCHECK_UNRESOLVED
|
|
94
|
+
for finding in verified.findings
|
|
95
|
+
)
|
|
96
|
+
result.mapping_coverage = MappingCoverage(
|
|
97
|
+
eligible=len(occurrences),
|
|
98
|
+
mapped=len(mapped_occurrences),
|
|
99
|
+
verified=len(verified.verified),
|
|
100
|
+
mismatched=mismatched,
|
|
101
|
+
unresolved=unresolved,
|
|
102
|
+
unmapped=len(unmapped_occurrences),
|
|
103
|
+
)
|
|
104
|
+
for occurrence in unmapped_occurrences:
|
|
105
|
+
candidates = suggest_sources(
|
|
106
|
+
occurrence, workbook, limit=profile.max_candidates
|
|
107
|
+
)
|
|
108
|
+
result.suggestions.append(
|
|
109
|
+
MappingSuggestion(
|
|
110
|
+
slide=occurrence.slide,
|
|
111
|
+
line=occurrence.line,
|
|
112
|
+
line_skeleton=occurrence.line_skeleton,
|
|
113
|
+
figure_index=occurrence.figure_index,
|
|
114
|
+
figure_raw=occurrence.figure.raw,
|
|
115
|
+
candidates=[SuggestedSource.from_candidate(candidate) for candidate in candidates],
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
period_findings = _period_reconciliation(workbook, deck)
|
|
120
|
+
result.findings.extend(period_findings)
|
|
121
|
+
mapping = result.mapping_coverage
|
|
122
|
+
result.coverage.extend(
|
|
123
|
+
[
|
|
124
|
+
CoverageItem(
|
|
125
|
+
check_id="excel-ppt-crosscheck",
|
|
126
|
+
label="Excel to PowerPoint figure mappings",
|
|
127
|
+
artifact="package",
|
|
128
|
+
state=(
|
|
129
|
+
CoverageState.CHECKED
|
|
130
|
+
if deck.charts_available
|
|
131
|
+
else CoverageState.DEGRADED
|
|
132
|
+
),
|
|
133
|
+
findings=len(verified.findings),
|
|
134
|
+
detail=(
|
|
135
|
+
f"{mapping.eligible} eligible; {mapping.mapped} mapped; "
|
|
136
|
+
f"{mapping.verified} verified; {mapping.mismatched} mismatched; "
|
|
137
|
+
f"{mapping.unresolved} unresolved; {mapping.unmapped} unmapped"
|
|
138
|
+
+ (
|
|
139
|
+
""
|
|
140
|
+
if deck.charts_available
|
|
141
|
+
else "; visible native chart labels unavailable"
|
|
142
|
+
)
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
CoverageItem(
|
|
146
|
+
check_id="package-period-reconciliation",
|
|
147
|
+
label="Excel and PowerPoint reporting-period reconciliation",
|
|
148
|
+
artifact="package",
|
|
149
|
+
state=CoverageState.CHECKED,
|
|
150
|
+
findings=len(period_findings),
|
|
151
|
+
),
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
return result
|