The interface is localized. Skill descriptions and documents are machine-translated by AI into your language; code, commands, paths and technical terms stay in the original, and the author's document governs.
Explore skills
Design & creative · SKILL.md

lineage-extract-static

lineage-extract-static

Use when extracting data/process lineage from a codebase or file tree — Python/Java/SQL/DSX/Control-M/dbt/COBOL/Pick-MultiValue-BASIC/scheduler configs/log files into spec-correct OpenLineage 2.0.2 JobEvent + DatasetEvent ndjson plus dual visual report (HTML Cytoscape DAG + Mermaid markdown + CSV). Skill is a framework — model-neutral prompts the in-session…

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill documentation

GitHub

name: lineage-extract-static description: "Use when extracting data/process lineage from a codebase or file tree — Python/Java/SQL/DSX/Control-M/dbt/COBOL/Pick-MultiValue-BASIC/scheduler configs/log files into spec-correct OpenLineage 2.0.2 JobEvent + DatasetEvent ndjson plus dual visual report (HTML Cytoscape DAG + Mermaid markdown + CSV). Skill is a framework — model-neutral prompts the in-session AI CLI (Claude Code, Codex CLI, Antigravity CLI (agy), Copilot CLI) uses to analyse chunks via its own LLM context, NOT per-format parser plug-ins. Static analysis only (v1); runtime producers in v2. Also trigger on "lineage report", "data flow extraction", "openlineage", "trace data through the codebase", "where does this dataset come from", "Java lineage", "JDBC/JPA lineage", "Pick/MultiValue lineage"." disambiguation: LLM-driven lineage across many stacks — Python, Java, SQL, DSX, scheduler configs. Deterministic no-LLM COBOL/JCL/EXEC-SQL parsing is mainframe-lineage-parsers.

lineage-extract-static — Cross-System Data/Process Lineage Skill

S033 design at docs/plans/2026-05-14-lineage-extract-static-skill-design.md.

What it does

Reads any file tree and emits:

  • openlineage.ndjson — canonical stream of JobEvent + DatasetEvent (one event per line). Spec-correct static lineage per OpenLineage 2.0.2 — NO synthesised Run by default. HARD-RULE 1.
  • openlineage.json — derived {"events": [...]} bundle for tooling that prefers single-document input.
  • lineage_edges.csv — single denormalized CSV: src_dataset_namespace, src_dataset_name, src_kind, target_job_namespace, target_job_name, target_job_kind, edge_kind, confidence, evidence_file, evidence_line, extractor_id. Opt-in --output-format=ol-relational splits into 4 CSVs.
  • report.html — self-contained Cytoscape DAG with sortable tables and download links. Air-gap safe (vendored cytoscape.min.js OR CDN fallback OR --no-vendor Mermaid-only).
  • report.md — GitHub-renderable Mermaid summary (flowchart capped at 50 nodes + truncation pointer to report.html for over-cap).
  • manifest.json — per-run metadata (run_id, costs, counters, redaction_count, errors). Validated against schemas/lineage-manifest.v1.json.
  • errors.jsonl — per-file extraction failures.

When to use it

Trigger on these user phrasings:

  • "build me a lineage report on /path/to/project"
  • "trace data flow through this codebase"
  • "where does this dataset come from"
  • "extract lineage to OpenLineage format"
  • "generate a data flow visualisation"
  • "what jobs read/write to this table"

Skip when:

  • The user wants RUNTIME lineage (job execution traces) — that's openlineage-spark / openlineage-airflow producers, v2.
  • The user wants COLUMN-level lineage — defer to v1.1 (needs schema metadata not available statically).
  • The user wants CROSS-REPO lineage — single project root only in v1.
  • The user wants BUSINESS / FUNCTIONAL lineage (semantic annotations on jobs) — post-v1 enrichment composing intent-extract.

Architecture (the LLM-driven framework)

The skill is the framework, the in-session AI CLI is the parser. There are NO per-format AST parsers (sqlglot / tree-sitter / xml.sax) in scripts/. The agent's LLM handles arbitrary input formats — Python, Java, SQL, DSX, Control-M, dbt, COBOL, Pick/MultiValue BASIC, log files — anything in its training coverage. The skill provides:

  • 5 model-neutral prompts in prompts/*.md — instructions the agent uses for per-chunk analysis, chunk merging, identity resolution, and redaction.
  • 8 deterministic Python scripts in scripts/*.py — chunk_file, accumulate, merge_into_ol, validate_ol, redact, project_views, render_report, install_vendor.
  • 6 reference docs in references/*.md — OL spec, confidence classifier, dataset identity, output formats, chunking strategy, anti-patterns.
  • 3 JSON schemas in schemas/ — lineage-finding.v1, lineage-manifest.v1, openlineage-2.0.2-vendored.

Scripts have NO LLM calls. Prompts have NO Python code. The two halves are interoperable across Claude Code / Codex CLI / Antigravity CLI (agy) / Copilot CLI because the prompts are model-neutral (HARD-RULE 8).

Invocation flow

When a user issues lineage-extract-static <project_root> (or equivalent NL: "build me a lineage report on /path/to/project"):

1. Agent reads this SKILL.md (the orchestration playbook).
2. Agent runs `scripts/chunk_file.py` over the project tree.
   Output: per-file `manifest.json` at ~/.cache/lineage-extract-static/runs/<run_id>/files/<file_sha256>/
   carrying path, sha256, size, line_count, chunked: bool, chunk_count.
3. For each file:
   3a. Small file (<= 5 MB / <= 20k lines) → agent reads the whole file,
       applies `prompts/analyze-file.md`, emits ONE `lineage-finding.v1`
       JSON object to `chunk_0001.jsonl` (single chunk).
   3b. Large file → agent reads chunk-by-chunk (default 2000 lines/chunk
       with 50-line overlap), applies `prompts/analyze-file.md` per chunk,
       writes per-chunk JSONL. Then applies
       `prompts/merge-chunks-within-file.md` to reconcile boundary_status
       markers; agent calls `scripts/accumulate.py` to perform the deterministic
       boundary-pairing predicate (NOT an LLM judgment) and emit the file-level
       rollup at `summary.json`.
4. After all files done, agent applies `prompts/merge-across-files.md`
   to combine per-file rollups + runs `prompts/resolve-identity.md` to
   canonicalize dataset URIs (3-step waterfall: SQL FQN → repo-rel path → alias map).
5. Agent runs `scripts/redact.py` on the combined rollup (fail-closed).
6. Agent runs `scripts/merge_into_ol.py` → emits openlineage.ndjson + openlineage.json
   + lineage_edges.csv to /tmp/lineage-extract-static-<session>/. JobEvents now
   carry a `sourceCodeLocation.contentSha256` JOB facet (sha256 of the RAW
   on-disk source bytes — the cross-engine v1.1 join key; see below). DatasetEvents
   carry a `schema` facet (SchemaDatasetFacet, 2026-07-01 uplift) when the rollup's
   `dataset_schemas` names the dataset's columns — named-in-source only (dbt
   schema.yml, SQL DDL, CSV headers, COBOL copybooks, staged SELECT lists), `type`
   only when stated, never invented (see references/output-formats.md).
   OUTPUT DatasetEvents additionally carry a `columnLineage` 1-2-0 facet
   (2026-07-02 uplift) when the rollup's `column_lineage` names per-column
   derivations (SQL rename lists, explicit passthroughs, expressions naming
   their sources; `select * from <single known parent>` expands 1:1 via the
   passthrough marker — multi-input or unknown-parent SELECT-* yields NOTHING),
   and a `documentation` facet when `dataset_descriptions` states one. Schema
   facet fields carry `description` when stated in source. No confidence key
   ever rides on facets — confidence stays in the edge/report layer.
   §9 corollary (a) (user-approved 2026-07-02): a `select * from <one known
   parent>` model with no own column set inherits the parent's NAMED set as
   its own schema facet (name + parent-stated type only) — deterministic SQL
   semantics, same single-input/known-parent constraints, never invented.
6b. Agent runs `scripts/project_views.py` → consumes the just-emitted
   openlineage.ndjson + lineage_edges.csv and emits `views.json` (the L1/L2
   render payload). DETERMINISTIC, stdlib, NO LLM — pure graph algebra. Skipped
   only if `--views=` is passed empty.
7. Agent runs `scripts/render_report.py` (ONCE, passing `views.json`) → produces
   the single self-contained `report.html` (a 3-tab L1/L2/L3-hidden Cytoscape
   switcher) + report.md + datasets.csv + jobs.csv + edges.csv (the OL-relational
   opt-in) to same dir.
8. Agent prints the location of the report directory to the user.

CLI flags

lineage-extract-static analyze <project_root>
  --output-dir <path>            (default: /tmp/lineage-extract-static-<session>/)
  --include-low-precision        (opt-in for plug-ins with precision_floor < 0.85)
  --output-format=ol-relational  (opt-in for 4-CSV split; default = single denormalized)
  --views=l1,l2                  (multi-view projection; DEFAULT ON. L1 = file
                                  interaction (job-retained); L2 = table/data +
                                  column. Pass --views= empty to disable. Folded
                                  into step 6b so render is called ONCE with the
                                  views.json payload.)
  --with-static-run              (opt-in for RunEvent-wrapped compatibility export)
  --merge-by-basename            (advisory speculative-confidence basename matching)
  --no-vendor                    (Mermaid-only output if Cytoscape vendor absent)
  --since <git-ref>              (incremental scan)
  --parallel N                   (per-file ProcessPoolExecutor workers; default = CPU)
  --aliases <path>               (alias map; default .lineage/aliases.yaml)

lineage-extract-static diff <baseline.json> <head.json>
  exit 0 = no breaking changes / exit 1 = datasets removed / exit 2 = jobs removed

HARD-RULEs

  1. Spec-correct OL output, NOT synthesised runs. Canonical emission is JobEvent + DatasetEvent with NO Run wrapper. RunEvent wrapping is opt-in via --with-static-run for consumer compatibility only. Phantom runs pollute downstream catalogs (Marquez, Atlan, DataHub).

  2. Bright-line confidence classifier. Any extraction whose evidence_snippet contains f-string syntax, .format(), %-format, env-var template, OR any symbol the LLM cannot resolve to a literal within the chunk → forced speculative. NEVER grounded when interpolation/dynamic-resolution is present. Enforced by the prompt template + post-emission validator in scripts/validate_ol.py.

  3. Identity waterfall pre-computed, never inferred by LLM. The 3-step resolution (SQL FQN → repo-root-relative path → alias map) runs in prompts/resolve-identity.md BEFORE OL emission. Basename-only merge OFF by default; advisory speculative only when --merge-by-basename is explicitly enabled.

  4. Secret redaction is fail-closed. scripts/redact.py runs on every aggregated rollup BEFORE OL emission. Any redaction error = abort, NEVER emit partial. Two-layer redaction: prompt-level (LLM instructed not to emit credentials) + post-LLM (regex-based scrubbing).

  5. Sandbox + cache discipline. Per-run cache at ~/.cache/lineage-extract-static/runs/<run_id>/ mode 0700 (NEVER /tmp). Atomic writes via .tmp.<pid> + os.replace(). TTL 30 days + LRU eviction at LINEAGE_CACHE_MAX_GB=10.

  6. DoS hard caps. LINEAGE_HARD_FILE_LIMIT_MB=50 skip-with-warn, LINEAGE_MAX_DURATION_S=3600 global wall-clock cap, LINEAGE_CACHE_MAX_GB=10 cache cap. Exit PARTIAL (not failure) when caps hit; surface in manifest.json.

  7. XSS-safe rendering. Every user-controlled string (file paths, dataset names, evidence snippets) interpolated into HTML via html.escape() server-side + textContent = / cy.json({elements}) (never any HTML-string sink — innerHTML / outerHTML / insertAdjacentHTML / document.write) client-side, on EVERY view tab. Required test test_html_escape_hostile_filenames_inert_across_tabs enforces this across the L1 and L2 element sets. Hostile filenames must NOT execute.

  8. Cross-tool portability. Prompts in prompts/*.md are model-neutral — no Anthropic-specific anchors, no Claude-only facets, no <> tags. Tested against Claude Code + Codex CLI + Gemini CLI (pre-retirement) + Copilot CLI; the gemini CLI retired 2026-06-18 — use Antigravity CLI (agy) instead.

Confidence taxonomy (bright-line classifier)

See references/confidence-classifier.md for worked examples. Quick reference:

TierRuleOL emission
groundedLiteral string token (path/table-name); all symbols resolve in the local context (function args, top-level constants)Solid edge; feeds gate decisions
inferredName-resolution heuristic — env-var resolved against in-repo .env / config.yaml; relative path resolved against __file__; SQL FROM <alias> resolved against same-file CTEDashed edge; advisory only
speculativeString interpolation (f-string / .format() / %-format / template literal) within ±20 lines; env-var without in-repo resolution; unresolved symbol; SELECT * without schema metadata; basename-only matchDotted edge; collapsed by default in HTML; never feeds gates

Identity resolution (3-step waterfall)

See references/dataset-identity.md for full rules. Quick reference:

  1. SQL FQN (highest precedence). namespace = <db-engine>://<host>:<port>/<db>, name = <schema>.<table>. Default schemas: PostgreSQL public, Oracle <USER>, Snowflake <account>.<warehouse>.
  2. Repo-root-relative absolute path (filesystem datasets). realpath to dereference symlinks. namespace = file://<repo-root-anchor>, name = <repo-relative path>.
  3. Configurable alias map (.lineage/aliases.yaml, optional). Resolves DSN strings, NFS mount mirrors, etc., to canonical URIs.

Basename-only merge is OFF by default. Behind --merge-by-basename flag (advisory), basename matches emit edges with confidence: speculative + possible_alias facet linking candidates — NEVER silently merged.

Output layout

/tmp/lineage-extract-static-<session>/
├── openlineage.ndjson          ← canonical OL stream (JobEvent + DatasetEvent per line)
├── openlineage.json            ← derived bundle {"events": [...]}
├── lineage_edges.csv           ← single denormalized CSV (default)
├── datasets.csv                ← OL-relational opt-in
├── jobs.csv                    ← OL-relational opt-in
├── edges.csv                   ← OL-relational opt-in
├── runs.csv                    ← OL-relational opt-in (only when --with-static-run)
├── manifest.json               ← per-run metadata (validated against lineage-manifest.v1)
├── errors.jsonl                ← per-file extraction failures
├── views.json                  ← L1/L2 render payload (project_views.py; default on)
├── report.md                   ← Mermaid summary (air-gap L1 fallback)
└── report.html                 ← single self-contained 3-tab (L1/L2/L3-hidden)
                                   Cytoscape switcher + sortable tables + downloads

Multi-view projection (L1 / L2) + the contentSha256 join key

scripts/project_views.py is a DETERMINISTIC, stdlib, NO-LLM post-pass that consumes the already-emitted openlineage.ndjson + lineage_edges.csv (the CSV carries the per-edge confidence/evidence the OL events drop, plus the schedules/depends_on edges that never reach OL). It rebuilds the typed bipartite graph, reattaches per-edge confidence/evidence, and emits two honest abstraction views plus a single views.json render payload:

  • L1 — file interaction (JOB-RETAINED). Filter to kind=file datasets but KEEP the job node (dataset → job → dataset). It does NOT collapse jobs and NEVER fabricates a file→file cross-product edge (locked user decision). Every edge keeps its CSV confidence + evidence.
  • L2 — table/data + column. Filter to kind ∈ {table,topic,queue}, keep job nodes. Column lineage is read from the columnLineage 1-2-0 facet (when present) and nested under its parent table edge, surfaced as an expand-on-click <details> table — never graph clutter. Tolerant of an absent facet (table-level L2 still works).
  • L3 — functional. Deferred to v1.1 (a separate forge cycle); the report reserves a hidden L3 tab.

render_report.py builds ONE self-contained report.html with ONE Cytoscape instance and a 3-tab .view-tabs switcher that swaps cy.json({elements}) between the pre-built L1/L2 sets from views.json. XSS-safe throughout: every embedded element set goes through the same html.escape + </>/&/U+2028/ U+2029 escape chain and is consumed only via cy.json / textContent — the template assigns NO HTML-string sink (innerHTML / outerHTML / insertAdjacentHTML / document.write) anywhere. Air-gap safe: with no vendor and no network, report.html is skipped and report.md carries the L1 Mermaid.

sourceCodeLocation.contentSha256 JOB facet (v1.1 join key, shipped now). merge_into_ol.py threads the already-computed file_sha256 (chunk_file.sha256_of_file) into each JobEvent's sourceCodeLocation facet. The byte definition is contentSha256 = sha256 of the RAW on-disk source file bytes — a streaming whole-file hash with NO encoding normalization, NO copybook/symbol expansion, NO chunk scoping. This definition is IDENTICAL to the mainframe-lineage-parsers engine's sourceCodeLocation.contentSha256 so the two streams join on the same key (the lineage JOB ↔ legacy-code-intel artifact join the deferred L3 view will use). The OL core spec stays pinned at 2.0.2 — the facet is purely additive and self-describing.

Anti-patterns — STOP if you catch yourself

  • Emitting Run by default — canonical OL spec for static lineage is JobEvent + DatasetEvent ONLY. RunEvent is opt-in (--with-static-run).
  • Inferring grounded confidence when evidence has f-string / .format() / %-format — these are forced speculative. HARD-RULE 2.
  • Merging datasets by basename alone — basename-only merge is OFF by default. When --merge-by-basename is enabled, the match still gets confidence: speculative + possible_alias facet.
  • Writing to /tmp for the run cache — ~/.cache/lineage-extract-static/runs/<run_id>/ mode 0700 ONLY. HARD-RULE 5.
  • Emitting partial output after a redaction error — scripts/redact.py is fail-closed. Any error aborts the run. HARD-RULE 4.
  • Synthesising a Run "for compatibility" without the user asking — --with-static-run is OPT-IN. Synthesising on every run pollutes catalogs.
  • Building per-format parsers in scripts/ — the LLM is the parser. v1 has NO sqlglot / tree-sitter / xml.sax plug-ins. If accuracy is insufficient for a specific format, add a deterministic plug-in in v1.1 against a frozen contract.
  • Hard-coding file extensions — chunk_file.py is format-agnostic (just I/O); the LLM decides which formats it can extract from. Unsupported formats produce gap: language_unsupported entries.

Cross-tool portability

Prompts in prompts/*.md are tested against all four AI CLIs:

  • Claude Code (Claude Opus 4.7)
  • Codex CLI (GPT-5.4)
  • Antigravity CLI (agy)
  • Copilot CLI (GPT-5.4 backend)

Run ~/.claude/skills/research-for-skills/cross-tool-portability/scripts/verify-skill-portability.sh to confirm. HARD-RULE 8.

Dependencies & environments (CLI / VS Code end users)

The core is pure stdlib and always runs — chunking, accumulation, the OpenLineage ndjson/CSV merge, redaction, and the L1/L2 view projection need zero third-party packages. The libraries in requirements-optional.txt are OPTIONAL enhancers; this skill never pip-installs at runtime.

See your situation first — the doctor never installs anything:

python3 scripts/check_deps.py

It prints the active interpreter, which enhancers are present/missing, what degrades when one is absent, a PEP-668-safe install recipe, and a sibling full-deps interpreter if one exists on the host.

EnhancerUnlocksIf absent
jinja2the self-contained report.html + the 3-tab L1/L2 view-switcherreport.html is skipped; report.md (Mermaid) is the air-gap fallback (L2 column detail is HTML-only)
jsonschemawrite-time OL 2.0.2 schema validationOL is written unvalidated

Install (only if you want the HTML report / validation). Modern distros mark the system python3 PEP-668 externally-managed, so a bare pip install is BLOCKED — use one of:

# Recommended — PEP-668-safe project venv (CLI or VS Code):
python3 -m venv .venv && .venv/bin/pip install -r requirements-optional.txt
# then run the skill's scripts with  .venv/bin/python

# Per-user (where allowed):   python3 -m pip install --user -r requirements-optional.txt
# Last resort (system py):    python3 -m pip install --break-system-packages -r requirements-optional.txt
# Air-gapped (offline wheels): python3 -m pip install --no-index --find-links=<wheel-dir> -r requirements-optional.txt

VS Code: select the venv interpreter as the workspace interpreter — the skill's scripts run under whatever python3 the CLI/extension invokes, so an env without jinja2 silently produces the Mermaid report.md instead of report.html. Run check_deps.py in the integrated terminal to confirm which interpreter is active.

Composition with other skills

  • wiring-extract-static — produces source-code call graph; lineage-extract-static can use wiring snapshots to enrich job-to-job depends_on edges (v1.1 enhancement).
  • intent-extract — adds per-job functional intent annotations (post-v1 enrichment for business / functional lineage).
  • intent-map-render — dual Mermaid + Cytoscape rendering precedent reused for scripts/render_report.py.
  • visual-companion — Cytoscape vendor templates extended at WP-7 (scripts/install_vendor.sh).
  • dep-currency-check — confidence_level pattern precedent (grounded / inferred / speculative).

See also

  • references/openlineage-spec-2.0.2.md — what to emit, with worked examples
  • references/confidence-classifier.md — bright-line grounded/inferred/speculative rules
  • references/dataset-identity.md — 3-step waterfall codified pre-code
  • references/output-formats.md — OL JSON + CSV + HTML + Mermaid contract
  • references/chunking-strategy.md — two-phase + boundary_status reconciliation
  • references/anti-patterns.md — synthetic Run, basename merge, scope creep

Security — XML / XHTML parsing

For HTML/XHTML rendering of downstream output (storage format → display), sanitise with bleach or nh3 BEFORE inserting into a browser context — never raw-render API-returned XHTML. See llm-security SKILL.md §4.4 for context-appropriate escaping rules.

Saved skills