Skip to content

Commit 026c80b

Browse files
authored
Merge pull request #121 from codellm-devkit/fix/issue-118-119-emit-gates
feat(cli)!: drop msgpack output; enforce --emit neo4j full-depth contract
2 parents baaa418 + 8b376b4 commit 026c80b

9 files changed

Lines changed: 160 additions & 130 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- **BREAKING: the msgpack output format is removed** (#118, TS parity): the
12+
`--format msgpack` CLI choice, the `analysis.msgpack` artifact, the msgpack
13+
serialization mixin on schema models, and the `msgpack` dependency are gone.
14+
`analysis.json` is the single wire format; anyone passing `--format msgpack`
15+
must drop the flag. `--format json` still works unchanged.
16+
- **`--emit neo4j` now enforces its always-full-depth contract** (#119): it
17+
runs at level 4 with every graph section regardless of defaults, and
18+
explicitly passing `-a`/`--graphs` alongside it is now the documented
19+
explicit error (previously accepted silently — and worse, the emission
20+
actually ran at the default level 1, producing a partial graph). Passing
21+
`--graphs` below `-a 3` is also now consistently rejected even when the
22+
value equals the default.
23+
1024
## [1.1.1] - 2026-07-27
1125

1226
### Fixed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ declared callables by their `can://` tree id, imported/builtin targets by a
135135

136136
### Two projections
137137

138-
1. **`analysis.json`** — the tree itself (the `Analysis` envelope above), also
139-
available as gzip'd msgpack.
138+
1. **`analysis.json`** — the tree itself (the `Analysis` envelope above); the
139+
single wire format (the msgpack variant was removed in 1.2.0, #118).
140140
2. **Neo4j** (`codeanalyzer/neo4j/`) — a near-identity projection, keyed on the
141141
same `can://` / global ordinal ids: containment → typed `PY_HAS_*` /
142142
`PY_DECLARES` edges (`PY_HAS_MODULE`, `PY_DECLARES`, `PY_HAS_METHOD`,

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ needs.
7070
checked in as `schema.neo4j.json` (`2.0.0`) and shipped with every release.
7171
- **Incremental cache** — per-file results are cached under `.codeanalyzer`; `--lazy` (default)
7272
reuses them, `--eager` forces a clean rebuild. `--ray` distributes the work across cores.
73-
- **Compact output** — canonical `analysis.json`, or binary `analysis.msgpack` for smaller artifacts.
73+
- **Compact output**one canonical `analysis.json` per run.
7474

7575
## Installation
7676

@@ -143,8 +143,7 @@ canpy --input /path/to/python/project
143143
```
144144

145145
With no `--output`, the analysis is printed to stdout as compact JSON; with `--output <dir>` it is
146-
written to `analysis.json` (or `graph.cypher` for `--emit neo4j`, or `analysis.msgpack` with
147-
`--format msgpack`) in that directory.
146+
written to `analysis.json` (or `graph.cypher` for `--emit neo4j`) in that directory.
148147

149148
### Options
150149

codeanalyzer/__main__.py

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def main(
8585
typer.Option(
8686
"-f",
8787
"--format",
88-
help="Output format for --emit json: json or msgpack.",
88+
help="Output format for --emit json: json.",
8989
case_sensitive=False,
9090
),
9191
] = OutputFormat.JSON,
@@ -141,26 +141,31 @@ def main(
141141
),
142142
] = None,
143143
analysis_level: Annotated[
144-
int,
144+
Optional[int],
145145
typer.Option(
146146
"-a",
147147
"--analysis-level",
148148
help="Analysis depth: 1=symbol table+Jedi call graph, 2=+PyCG call "
149149
"graph, 3=+native intraprocedural dataflow (CFG/PDG), "
150-
"4=+interprocedural SDG (param/summary edges, alias-aware DDG).",
150+
"4=+interprocedural SDG (param/summary edges, alias-aware DDG). "
151+
"[default: 1; incompatible with --emit neo4j, which is always "
152+
"full-depth]",
151153
min=1,
152154
max=4,
155+
show_default="1",
153156
),
154-
] = 1,
157+
] = None,
155158
graphs: Annotated[
156-
str,
159+
Optional[str],
157160
typer.Option(
158161
"--graphs",
159162
help="Level 3+ only: comma-separated program-graph sections to emit "
160163
"(cfg, dfg, pdg, sdg). Default: cfg,dfg,pdg. `dfg` emits the PDG's data "
161-
"edges only; `sdg` requires -a 4.",
164+
"edges only; `sdg` requires -a 4. Incompatible with --emit neo4j "
165+
"(always full-depth).",
166+
show_default="cfg,dfg,pdg",
162167
),
163-
] = "cfg,dfg,pdg",
168+
] = None,
164169
graph_field_depth: Annotated[
165170
int,
166171
typer.Option(
@@ -300,9 +305,40 @@ def main(
300305
_pin_hash_seed()
301306

302307
# Flag validation (strict: unrecognized values error out, never fall back).
303-
selected_graphs = [g.strip() for g in graphs.split(",") if g.strip()]
308+
# -a and --graphs use None sentinels so an explicitly-passed flag is
309+
# distinguishable from the default (#119).
310+
explicit_level = analysis_level is not None
311+
explicit_graphs = graphs is not None
312+
313+
# Neo4j is always full-depth (#119): the graph carries every level's
314+
# facts, so depth/section selectors cannot be combined with it — reject
315+
# explicitly-passed flags and force level 4 with every graph section.
304316
from codeanalyzer.dataflow.builder import VALID_GRAPHS
305317

318+
if emit == EmitTarget.NEO4J:
319+
explicit = [
320+
flag
321+
for flag, was_explicit in (
322+
("-a/--analysis-level", explicit_level),
323+
("--graphs", explicit_graphs),
324+
)
325+
if was_explicit
326+
]
327+
if explicit:
328+
logger.error(
329+
"--emit neo4j is always full-depth (level 4, all graph "
330+
f"sections); {' and '.join(explicit)} cannot be combined with it."
331+
)
332+
raise typer.Exit(code=2)
333+
analysis_level = 4
334+
graphs = ",".join(VALID_GRAPHS)
335+
336+
if analysis_level is None:
337+
analysis_level = 1
338+
if graphs is None:
339+
graphs = "cfg,dfg,pdg"
340+
selected_graphs = [g.strip() for g in graphs.split(",") if g.strip()]
341+
306342
unknown_graphs = [g for g in selected_graphs if g not in VALID_GRAPHS]
307343
if unknown_graphs:
308344
logger.error(
@@ -316,7 +352,7 @@ def main(
316352
if "sdg" in selected_graphs and analysis_level < 4:
317353
logger.error("--graphs sdg requires -a 4 (interprocedural SDG).")
318354
raise typer.Exit(code=2)
319-
if analysis_level < 3 and graphs != "cfg,dfg,pdg":
355+
if analysis_level < 3 and explicit_graphs:
320356
logger.error("--graphs is a level-3 option; pass -a 3 to emit program graphs.")
321357
raise typer.Exit(code=2)
322358
if analysis_level < 3 and graph_field_depth != 3:
@@ -408,16 +444,6 @@ def _write_output(artifacts, output_dir: Path, format: OutputFormat):
408444
f.write(json_str)
409445
logger.info(f"Analysis saved to {output_file}")
410446

411-
elif format == OutputFormat.MSGPACK:
412-
output_file = output_dir / "analysis.msgpack"
413-
msgpack_data = artifacts.to_msgpack_bytes()
414-
with output_file.open("wb") as f:
415-
f.write(msgpack_data)
416-
logger.info(f"Analysis saved to {output_file}")
417-
logger.info(
418-
f"Compression ratio: {artifacts.get_compression_ratio():.1%} of JSON size"
419-
)
420-
421447

422448
app = typer.Typer(
423449
callback=main,

codeanalyzer/config/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ class OutputFormat(str, Enum):
55
"""String-based enum for output formats to support typer case-insensitive options."""
66

77
JSON = "json"
8-
MSGPACK = "msgpack"

codeanalyzer/options/options.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
class OutputFormat(str, Enum):
88
JSON = "json"
9-
MSGPACK = "msgpack"
109

1110

1211
class EmitTarget(str, Enum):

0 commit comments

Comments
 (0)