|
| 1 | +# M1 Parser Risk Review |
| 2 | + |
| 3 | +## Scope |
| 4 | + |
| 5 | +This note reviews the first M1 parser slice only: a native Zig `dxt parse` |
| 6 | +implementation that loads a small dbt-style project, discovers Tier 0 resources, |
| 7 | +builds a partial graph, and writes an intentionally partial but dbt-shaped |
| 8 | +`manifest.json`. |
| 9 | + |
| 10 | +The slice should not claim compile compatibility, macro execution, adapter |
| 11 | +behavior, selector parity, or full artifact-schema coverage. Its value is a |
| 12 | +reviewable native parser path plus enough manifest structure for dbt-oracle |
| 13 | +comparison to start. |
| 14 | + |
| 15 | +## Dangerous Overclaims |
| 16 | + |
| 17 | +The highest risk is marketing or test language that says `dxt parse` is |
| 18 | +"dbt-compatible" before the parser can handle normal dbt project behavior. For |
| 19 | +M1, compatibility should be described as narrow and fixture-backed: |
| 20 | + |
| 21 | +- Safe claim: parses the supported Tier 0 fixture subset and emits a |
| 22 | + schema-shaped partial manifest. |
| 23 | +- Unsafe claim: parses dbt projects generally. |
| 24 | +- Safe claim: extracts simple literal `ref`, `source`, `config`, and `doc` |
| 25 | + calls from SQL/Jinja spans. |
| 26 | +- Unsafe claim: supports Jinja, macros, dispatch, or dbt parse-time semantics. |
| 27 | +- Safe claim: supports a documented subset of YAML used by the M1 fixtures. |
| 28 | +- Unsafe claim: supports YAML generally, including anchors, merges, custom tags, |
| 29 | + complex scalars, or every property file shape. |
| 30 | +- Safe claim: `ls` or selectors use the M1 graph for basic names, tags, paths, |
| 31 | + resource types, `+`, and `--exclude` only if tests prove those cases. |
| 32 | +- Unsafe claim: selector parity with dbt Core. |
| 33 | +- Safe claim: Jaffle Shop DuckDB is an exploratory parse target once Tier 0 |
| 34 | + passes. |
| 35 | +- Unsafe claim: Jaffle Shop support until dbt-oracle counts, IDs, dependency |
| 36 | + maps, and diagnostics are compared and pinned. |
| 37 | + |
| 38 | +Unsupported features should fail loudly with structured diagnostics. Silent |
| 39 | +skips are more dangerous than incomplete support because they produce artifacts |
| 40 | +that look authoritative while losing graph semantics. |
| 41 | + |
| 42 | +## Partial Manifest Strategy |
| 43 | + |
| 44 | +The M1 manifest should be partial by design but useful enough for downstream |
| 45 | +comparison. The practical rule is: emit dbt field names only when `dxt` can |
| 46 | +populate them with the intended meaning, and keep unknown `dxt` metadata out of |
| 47 | +the dbt object unless the target schema permits it. |
| 48 | + |
| 49 | +Minimum useful manifest content: |
| 50 | + |
| 51 | +- `metadata` with explicit `dxt` identity and the target dbt artifact schema |
| 52 | + family under test. |
| 53 | +- `nodes` for supported models, seeds, singular tests, and generated generic |
| 54 | + tests when M1 actually creates them. |
| 55 | +- `sources` for YAML-declared sources and tables. |
| 56 | +- `macros` only for parsed macro definitions, not executed macro behavior. |
| 57 | +- `docs` only for parsed docs blocks. |
| 58 | +- `exposures`, `metrics`, `groups`, and semantic resources as empty maps unless |
| 59 | + the parser has fixture-backed support for them. |
| 60 | +- `disabled` entries when disabled resources are discovered, not dropped. |
| 61 | +- `parent_map` and `child_map` derived from resolved refs and sources. |
| 62 | +- Stable `unique_id` values for all emitted resources. |
| 63 | +- Relative `path` and `original_file_path` values, with no host-specific paths. |
| 64 | + |
| 65 | +Fields that are easy to overstate should stay absent, null, or explicitly |
| 66 | +unsupported until implemented. Examples include compiled SQL, relation names, |
| 67 | +injected CTEs, macro dependency side effects, adapter-specific configs, catalog |
| 68 | +metadata, execution status, checksums if not calculated the dbt way, and |
| 69 | +unrendered config if the parser does not preserve it correctly. |
| 70 | + |
| 71 | +The manifest writer should be deterministic. Sort object keys and arrays where |
| 72 | +ordering is not semantically meaningful, normalize path separators, and avoid |
| 73 | +timestamps or invocation IDs in M1 unless tests normalize them. This keeps |
| 74 | +parity diffs focused on parser behavior instead of noise. |
| 75 | + |
| 76 | +## Python Runtime Boundary |
| 77 | + |
| 78 | +Python can remain a strong validation tool for M1, but not a product shortcut. |
| 79 | +The product path must be: |
| 80 | + |
| 81 | +```text |
| 82 | +dxt parse -> Zig CLI -> Zig project loader -> Zig parser -> Zig graph -> Zig artifact writer |
| 83 | +``` |
| 84 | + |
| 85 | +Acceptable Python: |
| 86 | + |
| 87 | +- invoking dbt Core as an oracle in tests, |
| 88 | +- generating synthetic fixtures, |
| 89 | +- normalizing and comparing JSON artifacts, |
| 90 | +- validating public artifact schemas, |
| 91 | +- scanning for local paths and secrets, |
| 92 | +- checking that Python product-runtime files were not introduced. |
| 93 | + |
| 94 | +Blocking Python uses: |
| 95 | + |
| 96 | +- product CLI command dispatch, |
| 97 | +- YAML parsing used by the `dxt` binary, |
| 98 | +- SQL/Jinja dependency extraction used by the `dxt` binary, |
| 99 | +- manifest writing used by the `dxt` binary, |
| 100 | +- selector evaluation used by the `dxt` binary, |
| 101 | +- spawning Python from `dxt parse`. |
| 102 | + |
| 103 | +The runtime-boundary check should grow with M1. It should fail on Python under |
| 104 | +product source paths and should also catch product command implementations that |
| 105 | +invoke `python`, `python3`, `dbt`, or developer harness scripts as the parser |
| 106 | +implementation. |
| 107 | + |
| 108 | +## No-Dependency Zig Scanner |
| 109 | + |
| 110 | +The first scanner should be byte-oriented, allocation-light, and deliberately |
| 111 | +smaller than a renderer. Its job is discovery, not full Jinja execution. |
| 112 | + |
| 113 | +Recommended shape: |
| 114 | + |
| 115 | +- Read each file once and scan bytes for Jinja delimiters: `{{`, `{%`, and |
| 116 | + `{#`. |
| 117 | +- Treat SQL text as opaque except for entering and leaving Jinja spans. |
| 118 | +- Inside expression or statement spans, tokenize only the subset needed for M1: |
| 119 | + identifiers, dots, string literals, commas, parentheses, equals, brackets, |
| 120 | + whitespace, comments, and simple number/bool/null literals if configs need |
| 121 | + them. |
| 122 | +- Recognize call forms by token sequence, not substring matching. This avoids |
| 123 | + false positives in comments, strings, similarly named functions, or words |
| 124 | + such as `reference`. |
| 125 | +- Support single-quoted and double-quoted strings with escapes before claiming |
| 126 | + literal `ref` or `source` extraction. |
| 127 | +- Skip Jinja comments entirely. |
| 128 | +- Record file offset, line, and column while scanning so diagnostics do not need |
| 129 | + a second full pass. |
| 130 | +- Use arenas for per-file tokens and discard them after producing compact |
| 131 | + dependency/config records. |
| 132 | +- Intern repeated package, resource, path, and config key strings in the graph |
| 133 | + allocator. |
| 134 | +- Avoid building a generic AST for every file in M1; keep the output as typed |
| 135 | + discovery records. |
| 136 | +- Bound recursion and token sizes. A malformed Jinja span should return a |
| 137 | + location-aware unsupported-syntax error, not allocate until OOM. |
| 138 | +- Make scanner behavior deterministic under parallel file walking by sorting |
| 139 | + discovered paths before parsing or sorting records before graph construction. |
| 140 | + |
| 141 | +Performance should be measured with simple budgets even in M1. A useful first |
| 142 | +gate is "parse each fixture file once, no quadratic rescans, no per-token heap |
| 143 | +allocation, no dependency on regex engines or external parser packages." Later |
| 144 | +benchmarks can compare against dbt Core, but M1 should at least include a |
| 145 | +repeat-parse stress test to catch leaks and accidental global state. |
| 146 | + |
| 147 | +## PR-Blocking Tests |
| 148 | + |
| 149 | +The first M1 parser PR should not merge unless these gates pass: |
| 150 | + |
| 151 | +- `zig build` |
| 152 | +- `zig build test` |
| 153 | +- black-box `dxt parse` tests against the built native binary |
| 154 | +- `pytest -q` for developer harness and safety tests |
| 155 | +- public-safety scan for local paths, secrets, logs, caches, and generated noise |
| 156 | +- runtime-boundary scan proving the product parser/artifact path is not Python |
| 157 | +- diff review confirming no generated artifacts, caches, or fixture outputs are |
| 158 | + committed accidentally |
| 159 | + |
| 160 | +Parser behavior tests that should block the PR: |
| 161 | + |
| 162 | +- Missing `dbt_project.yml` fails with a stable diagnostic and nonzero exit. |
| 163 | +- A minimal one-model project emits `target/manifest.json`. |
| 164 | +- Two models with a literal `ref` produce correct `unique_id`, `depends_on`, |
| 165 | + `parent_map`, and `child_map` entries. |
| 166 | +- A YAML source plus `source(...)` call produces a source entry and dependency. |
| 167 | +- YAML model properties populate descriptions, columns, tags, meta, and basic |
| 168 | + tests only for the supported subset. |
| 169 | +- A disabled model is represented as disabled and is not silently promoted to an |
| 170 | + active node. |
| 171 | +- Unsupported YAML or Jinja returns a structured unsupported-feature diagnostic. |
| 172 | +- Paths in artifacts are relative and portable. |
| 173 | +- Manifest output is deterministic across two consecutive parses. |
| 174 | +- `--project-dir` and `--target-path` are honored without leaking host paths. |
| 175 | +- Unknown or malformed parser flags still fail through the native CLI. |
| 176 | + |
| 177 | +Compatibility tests that should block the PR: |
| 178 | + |
| 179 | +- A dbt Core oracle run over Tier 0 fixtures is captured in developer-side tests. |
| 180 | +- Resource counts by type match the oracle for the supported fixture subset. |
| 181 | +- Stable IDs and dependency maps match the oracle where M1 claims support. |
| 182 | +- The emitted manifest validates against the pinned schema slice or documented |
| 183 | + partial schema harness used for M1. |
| 184 | +- Any expected mismatch is listed in a test fixture allowlist with a reason, not |
| 185 | + hidden in broad JSON normalization. |
| 186 | + |
| 187 | +Scanner-specific tests that should block the PR: |
| 188 | + |
| 189 | +- `ref` and `source` are not extracted from SQL comments, string literals, Jinja |
| 190 | + comments, or similarly named identifiers. |
| 191 | +- Single and double quoted literal calls work. |
| 192 | +- Package-qualified `ref("pkg", "model")` is either supported and tested or |
| 193 | + rejected with a clear diagnostic. |
| 194 | +- Malformed Jinja delimiters and unterminated strings fail predictably. |
| 195 | +- Large files with many non-Jinja bytes scan in linear time. |
| 196 | +- Repeated parse runs do not leak under Zig test allocator coverage. |
| 197 | + |
| 198 | +The PR is ready for review only when its description states exactly which dbt |
| 199 | +surfaces are supported, which are intentionally unsupported, which oracle |
| 200 | +version/schema slice was used, and the exact validation commands that passed. |
0 commit comments