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 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
+ """