Skip to content

Commit 0153d5c

Browse files
authored
Merge pull request #160 from codellm-devkit/feat/issue-157-artifacts-dependencies
feat: schema v2 artifacts, dependencies, and unresolved imports
2 parents 72ce5c5 + 8dd6222 commit 0153d5c

39 files changed

Lines changed: 3555 additions & 171 deletions

.claude/SCHEMA_DECISIONS.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,51 @@ made output load-dependent (#145).
285285
`callee: null→id` remains the single sanctioned L1→L2 refinement.
286286
- Neo4j projection unchanged (`PY_CALLS` carries `prov` as data).
287287

288+
## 2026-08-27 — Neutral `Artifact`/`Package` subgraph (issue #157, Task 6)
289+
290+
Design: `.superpowers/sdd/2026-08-27-artifacts-and-dependencies/task-6-brief.md`
291+
(gitignored working brief — not committed; this entry is the durable record).
292+
293+
Projects `PyApplication.artifacts`/`dependencies`/`unresolved_imports` (Tasks
294+
1-5) into the graph: new labels `Artifact`, `Package` (merge key `id`); new
295+
rels `HAS_ARTIFACT` (PyApplication→Artifact), `DECLARES_DEPENDENCY`
296+
(Artifact→Package, props `spec`/`kind`/`extras`/`prov`), `LOCKS`
297+
(Artifact→Package, prop `version`), `PY_PROVIDES` (Package→PyExternal),
298+
`PY_UNRESOLVED_IMPORT` (PyApplication→PyExternal, prop `prov`).
299+
300+
- **`Artifact`/`Package` deliberately break the `Py`-prefix convention** the
301+
Level-3 CPG section above establishes (`PySymbol`, `PyBodyNode`, `PY_CALLS`,
302+
…). Opposite rationale, same namespacing question: a manifest file or a
303+
PyPI package is not a Python-language concept — a TypeScript analyzer
304+
reading `package.json` in the same repo should MERGE onto the same
305+
`Artifact`/`Package` nodes, not create `TSArtifact`/`TSPackage` twins. The
306+
edges that stay this analyzer's own claim (`PY_PROVIDES` — "this analyzer
307+
resolved this import to this package", `PY_UNRESOLVED_IMPORT`) keep the
308+
`PY_` prefix; the nodes they connect to do not.
309+
- **`PY_PROVIDES`/`PY_UNRESOLVED_IMPORT` target ids are minted here, not
310+
looked up.** `app.external_symbols` only homes call-graph endpoints
311+
(`Codeanalyzer._home_external_symbols` walks `app.call_graph` alone), so a
312+
module that is imported but never called — the common case for
313+
`PyDependency.provides_imports`, and the *only* case for an unresolved
314+
import — has no existing `:PyExternal` ghost to MERGE onto. The projection
315+
builds one with the same id shape `_call_endpoint`/`_home_external_symbols`
316+
already use for a dot-less (no `.`) call-graph signature: `<app can:// id>
317+
/@external/<name>`, `module` absent — and the same two-label
318+
`["PySymbol", "PyExternal"]` RowBuilder idiom every other ghost in this file
319+
uses (schema declares `PyExternal`'s merge label as `PySymbol`). If a call
320+
into that same bare name is ever projected too, both rows collapse onto one
321+
node under `RowBuilder`'s MERGE-by-`(label, id)` semantics — correctly,
322+
since they name the same real-world symbol.
323+
- **`LOCKS` fans out to every lock artifact present**, not just the one that
324+
pinned a given dependency: `PyDependency.locked_version` merges all lock
325+
files' pins upstream (`build_dependency_view`) with no per-lock-file
326+
attribution left to project. One lock file is the overwhelmingly common
327+
case; revisit if a project with two conflicting lock files in one repo
328+
turns out to matter in practice.
329+
- Always projected regardless of `-a` — this section is L1 data, identical at
330+
every analysis level (mirrors `analysis.json`), consistent with Neo4j's
331+
existing full-depth-always posture for `--emit neo4j`.
332+
288333
## 2026-08-27 — Artifacts, dependencies, and the `can://artifact/` namespace
289334

290335
Design: `docs/design/specs/2026-08-27-artifacts-and-dependencies-design.md`.
@@ -305,7 +350,7 @@ at every level, like entrypoints.
305350
`--resolve-installed` flag.
306351
- **Neo4j**: neutral labels `:Artifact` / `:Package` (no `Py` prefix, shared
307352
MERGE targets across analyzers); `:Package.id` is a purl (`pkg:pypi/<name>`).
308-
`PY_PROVIDES` joins packages to the existing `:PyExternal` ghost ids, wiring
353+
`PY_PROVIDES` joins packages to existing-or-minted module-level `:PyExternal` ghosts, wiring
309354
dependencies into the call graph. New edges: `HAS_ARTIFACT`,
310355
`DECLARES_DEPENDENCY`, `LOCKS`, `PY_PROVIDES`, `PY_UNRESOLVED_IMPORT`.
311356
- Capture broad (config files as nodes with `roles`), extract narrow

.github/workflows/release.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,14 @@ jobs:
114114

115115
# Platform-independent, version-locked release assets published alongside the
116116
# wheels/sdist: the Neo4j schema contract (so a consumer can validate
117-
# producer/consumer compatibility without installing the package) and the
118-
# cargo-dist-style install script.
117+
# producer/consumer compatibility without installing the package), the
118+
# same contract's DDL as runnable Cypher, and the cargo-dist-style
119+
# install script.
119120
- name: Stage release assets (Neo4j schema + installer script)
120121
run: |
121122
mkdir -p release-assets
122123
uv run canpy --emit schema > release-assets/schema.json
124+
uv run python -c "from codeanalyzer.neo4j.schema import uniqueness_constraints, INDEXES; print('\n'.join(s + ';' for s in uniqueness_constraints() + INDEXES))" > release-assets/schema.cypher
123125
cp packaging/install/canpy-installer.sh release-assets/canpy-installer.sh
124126
ls -lh release-assets
125127

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,6 @@ node_modules/
195195
!.claude/
196196
.claude/*
197197
!.claude/SCHEMA_DECISIONS.md
198+
199+
# Track fixture lock files past the repo-root uv.lock ignore
200+
!test/fixtures/**/uv.lock

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Schema v2 now captures non-code artifacts (`application.artifacts`), declared
13+
dependencies with provenance (`application.dependencies`), and undeclared
14+
imports (`application.unresolved_imports`) at every analysis level (#157).
15+
Neo4j gains language-neutral `:Artifact`/`:Package` nodes (purl ids) joined
16+
to the existing `:PyExternal` ghosts. New flag: `--resolve-installed`.
17+
Lockfile-only (transitive) pins are emitted as `direct: false` dependency
18+
records attributed to the lock artifact (#152 reconciliation).
19+
- Artifact discovery never drops a file: every non-`.py` file is now
20+
inventoried, matched or not (unmatched decodable files as `text`/`unknown`,
21+
anything not UTF-8 decodable as `binary` with empty `source`). New
22+
`PyArtifact.text_truncated` field plus `--artifact-text/--no-artifact-text`
23+
and `--artifact-text-max-bytes` flags control verbatim `source` capture;
24+
`sha256`/`size_bytes` always reflect the full file regardless (#157).
25+
- The release workflow now also stages `schema.cypher` (the same Neo4j
26+
schema contract as runnable, `;`-terminated Cypher DDL -- uniqueness
27+
constraints plus indexes) as a GitHub Release asset alongside `schema.json`.
28+
829
## [1.2.0] - 2026-08-26
930

1031
### Changed

0 commit comments

Comments
 (0)