tcf-format 0.7.1__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.
- tcf/__init__.py +106 -0
- tcf/_core/__init__.py +13 -0
- tcf/_core/detect.pyx +153 -0
- tcf/auto_cadence.py +99 -0
- tcf/auto_min_len.py +89 -0
- tcf/column_features.py +88 -0
- tcf/composicional/__init__.py +27 -0
- tcf/composicional/hcc_seqrle.py +323 -0
- tcf/composicional/syntax.py +812 -0
- tcf/core/__init__.py +19 -0
- tcf/core/online.py +225 -0
- tcf/core/syntax_base.py +67 -0
- tcf/decoder.py +104 -0
- tcf/encoder.py +240 -0
- tcf/multi.py +588 -0
- tcf/natures/__init__.py +63 -0
- tcf/natures/templated_checked.py +199 -0
- tcf/natures/templated_padded.py +125 -0
- tcf/obat_shape.py +124 -0
- tcf/pipeline.py +60 -0
- tcf/schema.py +192 -0
- tcf/side_outputs.py +51 -0
- tcf_format-0.7.1.dist-info/METADATA +379 -0
- tcf_format-0.7.1.dist-info/RECORD +26 -0
- tcf_format-0.7.1.dist-info/WHEEL +4 -0
- tcf_format-0.7.1.dist-info/licenses/LICENSE +21 -0
tcf/__init__.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""TCF — Tabular Compact Format (pré-1.0; formato `#TCF.6` default, `#TCF.7`
|
|
2
|
+
opt-in — ADR-0024 versionamento pré-1.0).
|
|
3
|
+
|
|
4
|
+
API publica unificada (ADR-0014):
|
|
5
|
+
|
|
6
|
+
from tcf import encode, decode, SideOutputs
|
|
7
|
+
|
|
8
|
+
# Single-column (lista)
|
|
9
|
+
text = encode(["abc", "abcd", "abcde"])
|
|
10
|
+
values = decode(text)
|
|
11
|
+
assert values == ["abc", "abcd", "abcde"]
|
|
12
|
+
|
|
13
|
+
# Multi-column (dict)
|
|
14
|
+
text = encode({"id": ["1", "2"], "name": ["a", "b"]})
|
|
15
|
+
table = decode(text)
|
|
16
|
+
assert table == {"id": ["1", "2"], "name": ["a", "b"]}
|
|
17
|
+
|
|
18
|
+
# Side outputs opcional (debug, stats, schema)
|
|
19
|
+
side = SideOutputs()
|
|
20
|
+
text = encode(data, side_outputs=side)
|
|
21
|
+
print(side.hcc_trace) # detector iterations
|
|
22
|
+
print(side.column_features) # pre-pass features
|
|
23
|
+
# ... etc
|
|
24
|
+
|
|
25
|
+
Encoder dispatcha por tipo (list vs dict). Decoder dispatcha pelo
|
|
26
|
+
shebang (`#TCF.6 M` -> multi, senao -> single). Self-describing.
|
|
27
|
+
|
|
28
|
+
## Componentes canonicos
|
|
29
|
+
|
|
30
|
+
- `tcf.core.online`: **OBAT** (Online Bidirectional Affix Tokenizer).
|
|
31
|
+
Camada 1. Tokeniza strings via LCP + LCS. Codnome origem: `alg16`.
|
|
32
|
+
- `tcf.composicional.syntax`: **HCC** (Hierarchical Compositional
|
|
33
|
+
Coding). Camada 2. Detector unificado + emit composicional. Codnome
|
|
34
|
+
origem: `M8.A`.
|
|
35
|
+
- `tcf.composicional.hcc_seqrle`: HCC + seq-RLE near-identical (M10).
|
|
36
|
+
- `tcf.encoder` / `tcf.decoder`: API publica unificada.
|
|
37
|
+
- `tcf.multi`: implementacao interna multi-col + aliases deprecated.
|
|
38
|
+
- `tcf.side_outputs`: recipiente opcional pra debug/stats.
|
|
39
|
+
|
|
40
|
+
Ver `docs/algorithms/` para documentacao tecnica detalhada.
|
|
41
|
+
|
|
42
|
+
## Validacao
|
|
43
|
+
|
|
44
|
+
> Numeros abaixo sao probatorios: o TESTE mede, a prosa aponta. Guardioes
|
|
45
|
+
> byte-canonical: `tests/test_core_rt.py` + `tests/test_regression_v1_baseline.py`
|
|
46
|
+
> (baselines D1-D9/D17a), `tests/test_multi_col_rt.py` (multi-col),
|
|
47
|
+
> `tests/test_real_world_snapshots.py` (bytes reais; GATE de qualquer mudanca
|
|
48
|
+
> em pre-pass/OBAT/HCC). As % sao derivadas — ver o ADR citado em cada linha.
|
|
49
|
+
|
|
50
|
+
Single-column (M10 canonical, ADR-0011):
|
|
51
|
+
- D1-D9 sint: 1523B em 2981 raw = 51.1% ratio (RT 9/9)
|
|
52
|
+
[1523B pinado em test_core_rt.py + test_regression_v1_baseline.py]
|
|
53
|
+
- Real-world Adult+TPC-H 57 cols: -11.73% weighted vs M9 puro
|
|
54
|
+
[bytes em test_real_world_snapshots.py; % derivada, ADR-0011]
|
|
55
|
+
|
|
56
|
+
Multi-column (M10 + ADR-0013, T-EXP-MULTI-COL-SCALING):
|
|
57
|
+
- D17a sint 13x4: 322B INVARIANT (preservado vs EXP-011)
|
|
58
|
+
[322B pinado em test_multi_col_rt.py + test_regression_v1_baseline.py]
|
|
59
|
+
- Real-world 9 tabelas (Adult + TPC-H tier 1+2, 136k linhas):
|
|
60
|
+
-33.02% weighted vs raw, -31.46% vs single-col concat, RT 9/9
|
|
61
|
+
[bytes em test_real_world_snapshots.py; % derivada, ADR-0013]
|
|
62
|
+
|
|
63
|
+
## Backward compat
|
|
64
|
+
|
|
65
|
+
- `encode_table` / `decode_table` permanecem como aliases DEPRECATED
|
|
66
|
+
(emitem `DeprecationWarning`). Use `encode(dict)` / `decode(text)`.
|
|
67
|
+
|
|
68
|
+
Para historia: `experiments/lab/dirty/notas/historia-dirty-lab.md`.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
from tcf.decoder import decode
|
|
72
|
+
from tcf.encoder import encode
|
|
73
|
+
from tcf.multi import decode_table, encode_table # deprecated aliases
|
|
74
|
+
from tcf.natures import (
|
|
75
|
+
SPEC_CPF, SPEC_CNPJ, SPEC_IP,
|
|
76
|
+
TemplatedCheckedSpec, TemplatedPaddedSpec,
|
|
77
|
+
)
|
|
78
|
+
from tcf.pipeline import PipelineConfig
|
|
79
|
+
from tcf.schema import ColumnSchema, TableSchema, build_schema
|
|
80
|
+
from tcf.side_outputs import SideOutputs
|
|
81
|
+
|
|
82
|
+
# Pré-1.0 (ADR-0024): minor acompanha o formato (#TCF.7 -> 0.7); o PATCH (.1) e'
|
|
83
|
+
# contador de release/correcao, DESACOPLADO do comportamento (nao muda a logica
|
|
84
|
+
# nem o byte-output canonical). Sem compat rigida entre minors de dev; git e' o
|
|
85
|
+
# mecanismo de reproducao. v1.0 = release solido futuro.
|
|
86
|
+
__version__ = "0.7.1"
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"encode",
|
|
90
|
+
"decode",
|
|
91
|
+
"SideOutputs",
|
|
92
|
+
"build_schema",
|
|
93
|
+
"TableSchema",
|
|
94
|
+
"ColumnSchema",
|
|
95
|
+
# Natures (ADR-0015):
|
|
96
|
+
"TemplatedCheckedSpec",
|
|
97
|
+
"TemplatedPaddedSpec",
|
|
98
|
+
"SPEC_CPF",
|
|
99
|
+
"SPEC_CNPJ",
|
|
100
|
+
"SPEC_IP",
|
|
101
|
+
# Pipeline toggles (T-CODE-LAYERED-PIPELINE Fase 1):
|
|
102
|
+
"PipelineConfig",
|
|
103
|
+
# Deprecated (mantidos pra migracao):
|
|
104
|
+
"encode_table",
|
|
105
|
+
"decode_table",
|
|
106
|
+
]
|
tcf/_core/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""tcf._core — aceleradores compilados OPCIONAIS (Cython).
|
|
2
|
+
|
|
3
|
+
Cada modulo aqui tem fallback pure-Python no codigo canonical de src/tcf.
|
|
4
|
+
Se a extensao compilada nao estiver presente (install sem compilador), o
|
|
5
|
+
import falha silenciosamente e o pure-Python e' usado — output byte-identico.
|
|
6
|
+
|
|
7
|
+
Atual:
|
|
8
|
+
- detect.pyx -> _detect_compositions (H-PERF-06-v2 Fase B, ADR-0020).
|
|
9
|
+
Fallback: M8AVirtualRefsSyntax._detect_compositions em composicional/syntax.py.
|
|
10
|
+
|
|
11
|
+
Build: best-effort durante `pip install` (hatch_build.py); ou local via
|
|
12
|
+
`python -m build` / extensao compilada in-place.
|
|
13
|
+
"""
|
tcf/_core/detect.pyx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# cython: language_level=3, boundscheck=False, wraparound=False
|
|
2
|
+
"""Acelerador Cython OPCIONAL de _detect_compositions (HCC, H-PERF-06-v2 Fase B).
|
|
3
|
+
|
|
4
|
+
ADR-0020. Logica IDENTICA ao metodo pure-Python em
|
|
5
|
+
composicional/syntax.py (M8AVirtualRefsSyntax._detect_compositions, pos-weld
|
|
6
|
+
#15). Estruturas de dados continuam Python (Counter/dict/tuple/list) -> ordem
|
|
7
|
+
de insercao e tie-break first-wins preservados byte-exato. So' adiciona
|
|
8
|
+
cdef Py_ssize_t em contadores/comprimentos e cdef list nas listas quentes.
|
|
9
|
+
Genexprs usam j/y pra evitar clash de escopo com os cdef.
|
|
10
|
+
|
|
11
|
+
Se esta extensao nao compilar/importar, syntax.py usa o fallback pure-Python
|
|
12
|
+
(output byte-identico). Validado: D1-D9=1523B, D17a=322B, fixtures real-world.
|
|
13
|
+
Speedup ~2.1-2.3x no _detect_compositions.
|
|
14
|
+
"""
|
|
15
|
+
from collections import Counter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _detect_compositions(self, pieces_per_line, atom_count):
|
|
19
|
+
cdef Py_ssize_t next_alias = 1
|
|
20
|
+
cdef Py_ssize_t comp_acc_k = 0
|
|
21
|
+
alias_to_sub = {}
|
|
22
|
+
iter_traces = []
|
|
23
|
+
|
|
24
|
+
cdef Py_ssize_t li, a, b, i, K, R, n_refs
|
|
25
|
+
cdef Py_ssize_t n_est_ub, n_tam_min, n_tam, baseline, net, ub_net
|
|
26
|
+
cdef Py_ssize_t best_net, virtual_count, virt_pos, virt_alias
|
|
27
|
+
cdef Py_ssize_t alias_temp, virtual_id, ai
|
|
28
|
+
cdef list refs, new_refs, novos
|
|
29
|
+
cdef bint line_had_sub
|
|
30
|
+
|
|
31
|
+
while True:
|
|
32
|
+
contagem = Counter()
|
|
33
|
+
sub_first_line = {}
|
|
34
|
+
for li, pieces in enumerate(pieces_per_line):
|
|
35
|
+
if pieces is None:
|
|
36
|
+
continue
|
|
37
|
+
for p in pieces:
|
|
38
|
+
if p[0] == 'refs':
|
|
39
|
+
refs = p[1]
|
|
40
|
+
n_refs = len(refs)
|
|
41
|
+
for a in range(n_refs):
|
|
42
|
+
for b in range(a + 2, n_refs + 1):
|
|
43
|
+
sub = tuple(refs[a:b])
|
|
44
|
+
contagem[sub] += 1
|
|
45
|
+
if sub not in sub_first_line:
|
|
46
|
+
sub_first_line[sub] = li
|
|
47
|
+
|
|
48
|
+
# alias_first_line: primeiro li onde -alias aparece em body
|
|
49
|
+
alias_first_line = {}
|
|
50
|
+
for li, pieces in enumerate(pieces_per_line):
|
|
51
|
+
if pieces is None:
|
|
52
|
+
continue
|
|
53
|
+
for p in pieces:
|
|
54
|
+
if p[0] == 'refs':
|
|
55
|
+
for ref in p[1]:
|
|
56
|
+
if ref < 0:
|
|
57
|
+
ai = -ref
|
|
58
|
+
if ai not in alias_first_line:
|
|
59
|
+
alias_first_line[ai] = li
|
|
60
|
+
|
|
61
|
+
# cheap upper-bound prune + running-max inline (ADR-0019)
|
|
62
|
+
n_est_ub = max(2, len(str(atom_count + comp_acc_k + len(contagem) + 9)))
|
|
63
|
+
n_tam_min = len(str(atom_count + comp_acc_k + 1))
|
|
64
|
+
|
|
65
|
+
candidates = []
|
|
66
|
+
best = None
|
|
67
|
+
best_net = 0
|
|
68
|
+
for sub, R in contagem.items():
|
|
69
|
+
if R < 2:
|
|
70
|
+
continue
|
|
71
|
+
K = len(sub)
|
|
72
|
+
ub_net = (R - 1) * (K * n_est_ub + (K - 1) - n_tam_min)
|
|
73
|
+
if ub_net <= best_net:
|
|
74
|
+
continue
|
|
75
|
+
virtual_count = sum(1 for y in sub if y < 0)
|
|
76
|
+
if virtual_count > 1:
|
|
77
|
+
continue
|
|
78
|
+
if virtual_count == 1:
|
|
79
|
+
virt_pos = next(j for j, y in enumerate(sub) if y < 0)
|
|
80
|
+
if virt_pos > 0:
|
|
81
|
+
virt_alias = -sub[virt_pos]
|
|
82
|
+
if alias_first_line.get(virt_alias,
|
|
83
|
+
float('inf')) >= sub_first_line[sub]:
|
|
84
|
+
continue
|
|
85
|
+
baseline = self._estimate_baseline_chars(sub, atom_count, comp_acc_k)
|
|
86
|
+
n_tam = len(str(atom_count + comp_acc_k + K - 1))
|
|
87
|
+
if baseline <= n_tam:
|
|
88
|
+
continue
|
|
89
|
+
net = (R - 1) * (baseline - n_tam)
|
|
90
|
+
candidates.append((net, sub, R, baseline, n_tam))
|
|
91
|
+
if net > best_net:
|
|
92
|
+
best_net = net
|
|
93
|
+
best = (sub, R)
|
|
94
|
+
|
|
95
|
+
iter_info = {
|
|
96
|
+
'n_pairs': sum(1 for v in contagem.values() if v >= 2),
|
|
97
|
+
'n_candidates': len(candidates),
|
|
98
|
+
'candidates_sorted': sorted(candidates, reverse=True,
|
|
99
|
+
key=lambda c: c[0]),
|
|
100
|
+
'picked': best,
|
|
101
|
+
'iter_num': len(iter_traces) + 1,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if best is None:
|
|
105
|
+
iter_info['stopped'] = True
|
|
106
|
+
iter_traces.append(iter_info)
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
sub, R = best
|
|
110
|
+
alias_temp = next_alias
|
|
111
|
+
next_alias += 1
|
|
112
|
+
comp_acc_k += len(sub) - 1
|
|
113
|
+
alias_to_sub[alias_temp] = list(sub)
|
|
114
|
+
virtual_id = -alias_temp
|
|
115
|
+
iter_info['alias_temp'] = alias_temp
|
|
116
|
+
iter_info['lines_affected'] = []
|
|
117
|
+
iter_info['n_substituicoes'] = 0
|
|
118
|
+
|
|
119
|
+
K = len(sub)
|
|
120
|
+
for li in range(len(pieces_per_line)):
|
|
121
|
+
pieces = pieces_per_line[li]
|
|
122
|
+
if pieces is None:
|
|
123
|
+
continue
|
|
124
|
+
novos = []
|
|
125
|
+
line_had_sub = False
|
|
126
|
+
for p in pieces:
|
|
127
|
+
if p[0] != 'refs':
|
|
128
|
+
novos.append(p)
|
|
129
|
+
continue
|
|
130
|
+
refs = p[1]
|
|
131
|
+
new_refs = []
|
|
132
|
+
i = 0
|
|
133
|
+
n_refs = len(refs)
|
|
134
|
+
while i < n_refs:
|
|
135
|
+
if (i + K <= n_refs and tuple(refs[i:i + K]) == sub):
|
|
136
|
+
new_refs.append(virtual_id)
|
|
137
|
+
i += K
|
|
138
|
+
iter_info['n_substituicoes'] += 1
|
|
139
|
+
line_had_sub = True
|
|
140
|
+
else:
|
|
141
|
+
new_refs.append(refs[i])
|
|
142
|
+
i += 1
|
|
143
|
+
if new_refs:
|
|
144
|
+
novos.append(('refs', new_refs))
|
|
145
|
+
if line_had_sub:
|
|
146
|
+
iter_info['lines_affected'].append(li + 1)
|
|
147
|
+
pieces_per_line[li] = novos
|
|
148
|
+
|
|
149
|
+
iter_traces.append(iter_info)
|
|
150
|
+
if len(iter_traces) >= 99:
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
return alias_to_sub, iter_traces
|
tcf/auto_cadence.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Detect cadence — heuristica pre-pass pra OBAT shape-preserve hint.
|
|
2
|
+
|
|
3
|
+
CAMADA-0 pre-pass (ordem do pipeline: pre-pass -> OBAT em core/ -> HCC em
|
|
4
|
+
composicional/). Irmaos pre-pass: column_features, auto_min_len, obat_shape.
|
|
5
|
+
|
|
6
|
+
Welded canonical 2026-05-22 (T-CODE-PACOTE1-WELD-CANONICAL).
|
|
7
|
+
Origem: `experiments/lab/clean/EXP-010-tcf-delta-aware-prototype/auto_pre.py`
|
|
8
|
+
(welded 2026-05-17, refino real-world 2026-05-19 ADR-0008).
|
|
9
|
+
|
|
10
|
+
Heuristica 2-regras:
|
|
11
|
+
- Regra 1 (wrapper+counter): lengths uniformes nas primeiras N strings
|
|
12
|
+
+ LCP+LCS / length >= threshold em pares consecutivos
|
|
13
|
+
- Regra 2 (numeric high-cardinality, ADR-0008): todas primeiras 20
|
|
14
|
+
strings sao numericas + cardinalidade > 0.5
|
|
15
|
+
|
|
16
|
+
Quando dispara, encoder usa `processar_with_hint(unicas, min_len,
|
|
17
|
+
prefer_shape_consistency=True)` em vez de `processar(unicas, min_len)`
|
|
18
|
+
canonical.
|
|
19
|
+
|
|
20
|
+
Refatorado pra usar ColumnFeatures (H-DA-11c). Versao canonical
|
|
21
|
+
recebe `analyze_column(values)` ja' calculada — evita recomputar
|
|
22
|
+
features basicas.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from tcf.column_features import ColumnFeatures
|
|
28
|
+
from tcf.core.online import lcp_len, lcs_len
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detect_cadence_from_features(
|
|
32
|
+
features: ColumnFeatures,
|
|
33
|
+
strings_unicas: list[str],
|
|
34
|
+
n_sample: int = 5,
|
|
35
|
+
threshold: float = 0.7,
|
|
36
|
+
numeric_card_threshold: float = 0.5,
|
|
37
|
+
) -> tuple[bool, dict]:
|
|
38
|
+
"""Detecta se coluna tem cadencia estrutural via 2 regras.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
features: ColumnFeatures basico (avg_len, card, is_numeric, sample, ...)
|
|
42
|
+
strings_unicas: unicas pra calcular LCP/LCS em pares consecutivos
|
|
43
|
+
n_sample: tamanho sample regra 1 (lengths uniformes + LCP/LCS)
|
|
44
|
+
threshold: limiar LCP+LCS / length na regra 1 (default 0.7)
|
|
45
|
+
numeric_card_threshold: limiar cardinalidade regra 2 (default 0.5)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
(detectou, info) — info contem rule_hit + detalhes.
|
|
49
|
+
"""
|
|
50
|
+
info: dict = {
|
|
51
|
+
"n_strings_total": len(strings_unicas),
|
|
52
|
+
"n_sample": min(n_sample, len(strings_unicas)),
|
|
53
|
+
"threshold": threshold,
|
|
54
|
+
"numeric_card_threshold": numeric_card_threshold,
|
|
55
|
+
"rule_hit": None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if len(strings_unicas) < 2:
|
|
59
|
+
info["reason"] = "muito poucas strings (<2)"
|
|
60
|
+
return False, info
|
|
61
|
+
|
|
62
|
+
sample = strings_unicas[:min(n_sample, len(strings_unicas))]
|
|
63
|
+
lengths = [len(s) for s in sample]
|
|
64
|
+
info["lengths"] = lengths
|
|
65
|
+
uniform_length = (len(set(lengths)) == 1) and lengths[0] > 0
|
|
66
|
+
|
|
67
|
+
# ---- Regra 1: wrapper+counter ----
|
|
68
|
+
if uniform_length:
|
|
69
|
+
L = lengths[0]
|
|
70
|
+
ratios = []
|
|
71
|
+
for i in range(1, len(sample)):
|
|
72
|
+
a, b = sample[i - 1], sample[i]
|
|
73
|
+
lcp = lcp_len(a, b)
|
|
74
|
+
lcs = lcs_len(a, b)
|
|
75
|
+
ratio = (lcp + lcs) / L
|
|
76
|
+
ratios.append({"pair": i, "lcp": lcp, "lcs": lcs,
|
|
77
|
+
"ratio": round(ratio, 3)})
|
|
78
|
+
info["lcp_lcs_ratios"] = ratios
|
|
79
|
+
if ratios and all(r["ratio"] >= threshold for r in ratios):
|
|
80
|
+
avg = sum(r["ratio"] for r in ratios) / len(ratios)
|
|
81
|
+
info["rule_hit"] = "1-uniform-length-high-lcp-lcs"
|
|
82
|
+
info["reason"] = f"L={L}, all ratios >= {threshold} (avg={avg:.2f})"
|
|
83
|
+
return True, info
|
|
84
|
+
|
|
85
|
+
# ---- Regra 2: numeric high-cardinality (ADR-0008) ----
|
|
86
|
+
# Cardinalidade computada sobre values (rows) em ColumnFeatures
|
|
87
|
+
info["cardinality"] = round(features.cardinality, 3)
|
|
88
|
+
info["is_numeric"] = features.is_numeric
|
|
89
|
+
|
|
90
|
+
if features.is_numeric and features.cardinality > numeric_card_threshold:
|
|
91
|
+
info["rule_hit"] = "2-numeric-high-cardinality"
|
|
92
|
+
info["reason"] = (
|
|
93
|
+
f"numeric, cardinality={features.cardinality:.3f} "
|
|
94
|
+
f"> {numeric_card_threshold}"
|
|
95
|
+
)
|
|
96
|
+
return True, info
|
|
97
|
+
|
|
98
|
+
info["reason"] = "nenhuma regra acionou"
|
|
99
|
+
return False, info
|
tcf/auto_min_len.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Auto-detect min_len por coluna (canonical, ADR-0010, H-DA-11).
|
|
2
|
+
|
|
3
|
+
CAMADA-0 pre-pass (ordem do pipeline: pre-pass -> OBAT em core/ -> HCC em
|
|
4
|
+
composicional/). Irmaos pre-pass: column_features, auto_cadence, obat_shape.
|
|
5
|
+
|
|
6
|
+
Heuristica v3 (decision tree shallow em avg_len + cardinality + is_numeric)
|
|
7
|
+
capturou 99.5% do oracle real-world em Adult+TPC-H (sub-exp
|
|
8
|
+
`experiments/lab/dirty/2026-05-21-h-da-11-auto-min-len/02-heuristica-v1/`).
|
|
9
|
+
A heuristica so' dispara n>=100 (real-world); seu efeito em bytes e' guardado
|
|
10
|
+
por `tests/test_real_world_snapshots.py` (GATE). O 99.5% e' derivado do
|
|
11
|
+
sub-exp acima, nao pinado como literal.
|
|
12
|
+
|
|
13
|
+
Gating `n >= 100`: datasets pequenos (D1-D9, exemplos sinteticos) usam
|
|
14
|
+
default ml=3 — preserva M9 baseline EXATO (1615B). O caminho do gating
|
|
15
|
+
(D1-D9 byte-canonical) e' guardado por `tests/test_core_rt.py`.
|
|
16
|
+
|
|
17
|
+
API:
|
|
18
|
+
- `detect_min_len_from_features(features, n_threshold=100)` — heuristica
|
|
19
|
+
pura que recebe `ColumnFeatures` ja' calculada
|
|
20
|
+
- `detect_min_len(values, n_threshold=100)` — wrapper backward compat
|
|
21
|
+
que chama `analyze_column(values)` internamente
|
|
22
|
+
|
|
23
|
+
Welded canonical 2026-05-22 (T-EXP-H-DA-11). Refatorado pra usar
|
|
24
|
+
ColumnFeatures unificado em 2026-05-22 (T-CODE-H-DA-11c).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from tcf.column_features import ColumnFeatures, analyze_column
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def detect_min_len_from_features(
|
|
33
|
+
features: ColumnFeatures, n_threshold: int = 100
|
|
34
|
+
) -> int:
|
|
35
|
+
"""Detecta min_len otimo a partir de ColumnFeatures.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
features: ColumnFeatures ja' calculada via analyze_column
|
|
39
|
+
n_threshold: limite inferior de rows pra aplicar heuristica
|
|
40
|
+
(default 100 — datasets menores usam ml=3 default, preserva
|
|
41
|
+
M9 baseline)
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
int em {3, 4, 5, 6}.
|
|
45
|
+
|
|
46
|
+
Heuristica v3 (decision tree shallow):
|
|
47
|
+
- n < n_threshold: 3 (gating)
|
|
48
|
+
- card < 0.2: 3 (baixa-card seguro)
|
|
49
|
+
- avg >= 25: 6 (long-form)
|
|
50
|
+
- avg >= 8 + card >= 0.4: 6 (dates, mid-len high-card)
|
|
51
|
+
- avg >= 5 + is_num + card >= 0.8: 6 (numeric high-card)
|
|
52
|
+
- avg >= 12 + card >= 0.7: 5 (c_phone)
|
|
53
|
+
- avg >= 3 + card >= 0.2: 4 (IDs sequenciais)
|
|
54
|
+
- else: 3
|
|
55
|
+
"""
|
|
56
|
+
if features.n_rows < n_threshold:
|
|
57
|
+
return 3
|
|
58
|
+
|
|
59
|
+
avg_len = features.avg_len
|
|
60
|
+
card = features.cardinality
|
|
61
|
+
is_num = features.is_numeric
|
|
62
|
+
|
|
63
|
+
if card < 0.2:
|
|
64
|
+
return 3
|
|
65
|
+
if avg_len >= 25:
|
|
66
|
+
return 6
|
|
67
|
+
if avg_len >= 8 and card >= 0.4:
|
|
68
|
+
return 6
|
|
69
|
+
if avg_len >= 5 and is_num and card >= 0.8:
|
|
70
|
+
return 6
|
|
71
|
+
if avg_len >= 12 and card >= 0.7:
|
|
72
|
+
return 5
|
|
73
|
+
if avg_len >= 3 and card >= 0.2:
|
|
74
|
+
return 4
|
|
75
|
+
return 3
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def detect_min_len(values: list[str], n_threshold: int = 100) -> int:
|
|
79
|
+
"""Backward-compat wrapper: analisa values e retorna min_len.
|
|
80
|
+
|
|
81
|
+
Equivale a:
|
|
82
|
+
detect_min_len_from_features(analyze_column(values), n_threshold)
|
|
83
|
+
|
|
84
|
+
Mantido para callers que nao tem ColumnFeatures pre-computado.
|
|
85
|
+
Para pipelines novos com multiplas heuristicas, preferir chamar
|
|
86
|
+
`analyze_column(values)` uma vez e passar para cada
|
|
87
|
+
`detect_X_from_features`.
|
|
88
|
+
"""
|
|
89
|
+
return detect_min_len_from_features(analyze_column(values), n_threshold)
|
tcf/column_features.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""ColumnFeatures — pre-pass unificado de features de coluna.
|
|
2
|
+
|
|
3
|
+
CAMADA-0 pre-pass (ordem do pipeline: pre-pass -> OBAT em core/ -> HCC em
|
|
4
|
+
composicional/). E' o produtor de features que os irmaos pre-pass
|
|
5
|
+
(auto_cadence, auto_min_len, obat_shape) consomem.
|
|
6
|
+
|
|
7
|
+
Calcula features basicas em 1 passada O(N) sobre values:
|
|
8
|
+
- n_rows, n_unicas
|
|
9
|
+
- avg_len, cardinality
|
|
10
|
+
- is_numeric (sample check)
|
|
11
|
+
- sample (primeiras N strings)
|
|
12
|
+
|
|
13
|
+
Heuristicas downstream (detect_min_len, detect_cadence, futuras
|
|
14
|
+
detect_X de naturezas pre-tx) recebem ColumnFeatures imutavel e
|
|
15
|
+
escolhem comportamento. Reduz duplicacao + permite reuso.
|
|
16
|
+
|
|
17
|
+
Conexoes:
|
|
18
|
+
- ADR-0010 (auto-detect min_len) — usa ColumnFeatures
|
|
19
|
+
- ADR-0008 (detect_cadence) — quando weldar canonical, usara' ColumnFeatures
|
|
20
|
+
- H-DA-11c — motivacao desta unificacao
|
|
21
|
+
|
|
22
|
+
Welded 2026-05-22 (T-CODE-H-DA-11c).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_numeric_string(v: str) -> bool:
|
|
31
|
+
"""Aceita int, float, negativos. Rejeita empty."""
|
|
32
|
+
if not v:
|
|
33
|
+
return False
|
|
34
|
+
try:
|
|
35
|
+
float(v)
|
|
36
|
+
return True
|
|
37
|
+
except (ValueError, TypeError):
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ColumnFeatures:
|
|
43
|
+
"""Features imutaveis extraidas de uma coluna de strings.
|
|
44
|
+
|
|
45
|
+
Computado por `analyze_column(values)` em 1 passada O(N).
|
|
46
|
+
"""
|
|
47
|
+
n_rows: int
|
|
48
|
+
n_unicas: int
|
|
49
|
+
avg_len: float
|
|
50
|
+
cardinality: float
|
|
51
|
+
is_numeric: bool
|
|
52
|
+
sample: tuple[str, ...] # tuple pra ser hashable (dataclass frozen)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def analyze_column(values: list[str], sample_size: int = 20) -> ColumnFeatures:
|
|
56
|
+
"""Calcula features basicas de uma coluna em 1 passada.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
values: lista de strings da coluna (com possiveis duplicatas)
|
|
60
|
+
sample_size: tamanho do sample pra check is_numeric (default 20)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
ColumnFeatures imutavel.
|
|
64
|
+
|
|
65
|
+
Edge cases:
|
|
66
|
+
- values vazio: retorna features zerados, is_numeric=False
|
|
67
|
+
- is_numeric: True so' se TODAS as strings do sample parsam float
|
|
68
|
+
"""
|
|
69
|
+
n = len(values)
|
|
70
|
+
if n == 0:
|
|
71
|
+
return ColumnFeatures(
|
|
72
|
+
n_rows=0, n_unicas=0, avg_len=0.0,
|
|
73
|
+
cardinality=0.0, is_numeric=False, sample=(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
n_unicas = len(set(values))
|
|
77
|
+
avg_len = sum(len(v) for v in values) / n
|
|
78
|
+
sample = tuple(values[:min(sample_size, n)])
|
|
79
|
+
is_num = all(_is_numeric_string(v) for v in sample) if sample else False
|
|
80
|
+
|
|
81
|
+
return ColumnFeatures(
|
|
82
|
+
n_rows=n,
|
|
83
|
+
n_unicas=n_unicas,
|
|
84
|
+
avg_len=avg_len,
|
|
85
|
+
cardinality=n_unicas / n,
|
|
86
|
+
is_numeric=is_num,
|
|
87
|
+
sample=sample,
|
|
88
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""HCC — Hierarchical Compositional Coding.
|
|
2
|
+
|
|
3
|
+
Camada 2 do TCF. Consome tokens raiz de OBAT e produz texto TCF
|
|
4
|
+
compacto via:
|
|
5
|
+
- Detector iterativo greedy de sub-tuplas reusaveis
|
|
6
|
+
- Emit com operadores `~` (cria ref auto-nomeado) e `,` (concat efemero)
|
|
7
|
+
- Pairwise left-assoc binarization
|
|
8
|
+
- Range `a..b` como caso particular de composicao por sequencia
|
|
9
|
+
- Output: sem brackets, LF only
|
|
10
|
+
|
|
11
|
+
`syntax.py` e' adaptacao byte-exata em logica de
|
|
12
|
+
`experiments/lab/dirty/old/2026-05-16-M8-virtual-refs-clean-output/M8-A-detector-unificado/syntax.py`
|
|
13
|
+
(codnome de origem: `M8.A`).
|
|
14
|
+
|
|
15
|
+
Adaptacoes vs original (welding step 2, 2026-05-17):
|
|
16
|
+
- `from online import ...` → `from tcf.core.online import ...`
|
|
17
|
+
- `from syntax_base import ...` → `from tcf.core.syntax_base import ...`
|
|
18
|
+
- removido `sys.path.insert(...)` (Python package resolve naturalmente)
|
|
19
|
+
|
|
20
|
+
Logica de encode/decode permanece byte-exata. Validado por M12 + M13 + M14.
|
|
21
|
+
|
|
22
|
+
Ver `docs/algorithms/HCC.md` para detalhamento (estrutura,
|
|
23
|
+
sub-linguagem matematica, body-order constraint, diferencial vs
|
|
24
|
+
Re-Pair / Sequitur / LZW).
|
|
25
|
+
|
|
26
|
+
Para uso via API publica: `from tcf import encode, decode`.
|
|
27
|
+
"""
|