Spec
docs/design/specs/repository-artifact-layer.md (committed in codellm-devkit/.github)
Summary
Adds a repository-artifact layer to canonical schema v2 across the three analyzers: the producer-side evidence a cross-service / whole-application analysis needs but no analyzer emits today. Schema impact is additive — three new application-contained node kinds (artifact, dependency, config_key), one new typed edge (config_use / *_USES_CONFIG), their Neo4j projections, and one shared scope enum with its ecosystem mapping table. Nothing existing is renamed or repurposed. This is roadmap group D (candidates 15–18); it lands into the v2 catalog group A stands up, so group A gates it.
Affected repos
codeanalyzer-schema — v2 model definitions (three node kinds + config_use edge + scope mapping table), Neo4j contract projection. Gates all analyzers.
codeanalyzer-python — :PyArtifact / :PyDependency / :PyConfigKey emitter, non-source discovery walk, pyproject.toml / requirements* / lockfile parsing, PY_USES_CONFIG
codeanalyzer-typescript — :TSArtifact / :TSDependency / :TSConfigKey, package.json (incl. dev/peer/optional) + lockfiles, tsconfig.json JSONC tolerance, TS_USES_CONFIG
codeanalyzer-java — :JArtifact / :JDependency / :JConfigKey, pom.xml / Gradle coordinate parsing (net-new), config_use in analysis.json (Neo4j J_USES_CONFIG deferred)
python-sdk — v2 Pydantic model mirror (co-evolution rule); facade surface exposure is later, out of scope here
Design decisions
- Artifacts are first-class nodes in the containment tree, not top-level sibling maps. The canonical keystone allows one node vocabulary reachable from
application by containment; sibling maps would introduce a second. This is a strict superset of the source proposal — the two string back-references (dependency→manifest, config→file) become containment edges, artifacts get durable can://…/@artifact/… ids, and the JSON matches the Neo4j graph the source doc already drew.
artifact_kind is a typed closed enum (build_manifest | dependency_lockfile | configuration | deployment_manifest | container | infrastructure | ci | script | documentation | data | other). other is the catch-all — coverage over classification; a file is never dropped for lack of a parser.
- Raw text captured by default, 256 KiB cap,
--artifact-text / --no-artifact-text flag; path + content_hash always present so a truncated/binary artifact still dereferences. Emit contract, not schema-versioned.
scope enum + ecosystem mapping table are coined once in the schema, not per-analyzer: runtime | development | test | build | optional | unknown, with the Maven/npm/pypi mapping in the contract.
config_use (candidate 18) is scoped in with a per-projection split, not deferred whole. The edge name and its (namespace, key) join are identical across all three languages and both projections. analysis.json carries it for all three; Neo4j carries PY_USES_CONFIG + TS_USES_CONFIG now.
- Scope guard:
J_USES_CONFIG (Java's Neo4j config-use edge) is explicitly OUT — Java's Neo4j projector has no body-node label until its v1→v2 migration (candidate 9). Obsolete as of 2026-08-29: candidate 9 landed — codeanalyzer-java#203 shipped the canonical v2 graph projection (V2GraphProjector, V2SchemaCatalog, graph contract 2.0.0, since 2.1.0), removed the --schema v2 --emit neo4j guard, made v2 the default schema, and codeanalyzer-java#205 followed. Java's projector does have body-node labels now. Whether J_USES_CONFIG is in or out is therefore a live decision for whoever picks up codeanalyzer-java#197, not a settled deferral. SDK facade/query surface for the artifact layer is OUT (later). This spec implements no analyzer — that is the codeanalyzer-backend rung, per repo, after the epic exists.
Release plan
codeanalyzer-schema lands first — the v2 models, config_use edge, scope table, Neo4j contract. Gates everything.
- The three analyzers implement in parallel, each on its own release train — none gates another. Per analyzer, units (artifact inventory → dependencies → config definitions → config uses) may land as separate PRs.
python-sdk mirrors the v2 models after any ONE analyzer is conformant — it does not wait for all three.
J_USES_CONFIG rides Java's candidate-9 v1→v2 Neo4j migration, not this epic. Obsolete — that migration has landed (see the scope guard above). Re-decide with codeanalyzer-java#197 rather than inheriting the deferral.
- Blocked by group A (the v2 catalog these node kinds land into must exist first).
Definition of done (epic-level)
- Every sub-issue closed and its PR's gate green.
artifact / dependency / config_key node kinds and the config_use edge defined once in codeanalyzer-schema with identical field names, enum values, and edge payload across python / java / typescript (parity clause holds — no renamed/repurposed shared vocabulary).
- The
scope enum and its ecosystem mapping table are encoded in the schema, not per-analyzer.
- Each analyzer's output validates against the v2 models;
analysis.json round-trips through the schema for all three languages.
PY_USES_CONFIG and TS_USES_CONFIG present in Neo4j. J_USES_CONFIG is no longer automatically a gap — Java's v2 projector exists as of codeanalyzer-java#203, so either it ships too or its absence is a recorded decision, not an inherited blocker.
python-sdk v2 models mirror the three node kinds and the edge.
- Existing source/program-graph behavior unchanged; docs / CHANGELOG updated.
Contract as shipped (python v1.3.0, 2026-08-29) — supersedes the decisions above where they differ
The python train implemented the full layer (issues codellm-devkit/codeanalyzer-python#152, #157, #162, #165; released as 1.3.0 on PyPI). Where implementation experience diverged from this epic's sketch, the shipped contract is the one siblings copy — rationale in the reconciliation and the settled-vocabulary comment below:
- Neutral nouns, prefixed claims — graph labels
Artifact / Package / ConfigKey carry no language prefix (a Dockerfile or a PyPI package is not a language concept; two analyzers over one repo MERGE onto one node). Language-specific edges keep the prefix: PY_PROVIDES, PY_UNRESOLVED_IMPORT, *_USES_CONFIG per language. This replaces PyArtifact/TSArtifact/JArtifact label triplets.
- Ids:
can://artifact/<app>/<path> (language-neutral namespace, not …/@artifact/… under the language); packages by purl (pkg:pypi/…, pkg:npm/…, pkg:maven/…); config keys <artifact-id>@key/<dotted.key>. Precondition for joins: agreed --app-name.
- Placement:
application.artifacts sibling map (config keys nested per artifact) rather than containment-tree nodes — the tree stays code-only.
- Classification: open
roles: string[] vocabulary (dependency-manifest, service-topology, container-image, ci, env, tool-config, packaging, script, docs, legal, iac, unknown) instead of the closed artifact_kind enum; never-drop still holds (unmatched decodable files → unknown, binaries inventoried hash-only).
- Dependency fields:
kind ∈ {runtime, dev, optional, build} (in place of the scope enum), prov ∈ {declared, lockfile, installed-metadata, heuristic}, direct: false for lockfile-only transitives, locked_version, provides_imports.
- Text policy: capture-on by default, 256 KiB cap,
--artifact-text/--no-artifact-text, --artifact-text-max-bytes; sha256/size always full-file; manifests/config parse from full on-disk text so extraction never depends on the stored (possibly truncated) copy.
- config_use is level-graded and never guesses: literal tier at the call-graph level, dataflow tiers over each analyzer's own def-use substrate; unresolved reads are first-class records with reasons (
non-literal / undefined-key). Deployment-env sources (Dockerfile ENV, compose/k8s env) mint bindable env-namespace keys; Dockerfile ARG gets a non-bindable dockerfile namespace.
- Neo4j
SCHEMA_VERSION holds at 2.0.0 until the layer settles across analyzers — one bump then.
- Release-plan amendment: the
codeanalyzer-schema-repo-first gate did not hold — python shipped in-repo models. Extracting shared models into codeanalyzer-schema remains open, now with a working reference to extract from.
Reference implementation to copy from: specs docs/design/specs/2026-08-27-artifacts-and-dependencies-design.md, 2026-08-28-config-key-family-design.md, 2026-08-28-config-use-edge-design.md (all in codeanalyzer-python); the v1.3.0 release assets include the machine-readable contract (schema.json, schema.cypher) and the query cookbook (analyzing-canpy-graphs-skill.tar.gz).
Spec
docs/design/specs/repository-artifact-layer.md(committed incodellm-devkit/.github)Summary
Adds a repository-artifact layer to canonical schema v2 across the three analyzers: the producer-side evidence a cross-service / whole-application analysis needs but no analyzer emits today. Schema impact is additive — three new application-contained node kinds (
artifact,dependency,config_key), one new typed edge (config_use/*_USES_CONFIG), their Neo4j projections, and one sharedscopeenum with its ecosystem mapping table. Nothing existing is renamed or repurposed. This is roadmap group D (candidates 15–18); it lands into the v2 catalog group A stands up, so group A gates it.Affected repos
codeanalyzer-schema— v2 model definitions (three node kinds +config_useedge +scopemapping table), Neo4j contract projection. Gates all analyzers.codeanalyzer-python—:PyArtifact/:PyDependency/:PyConfigKeyemitter, non-source discovery walk,pyproject.toml/requirements*/ lockfile parsing,PY_USES_CONFIGcodeanalyzer-typescript—:TSArtifact/:TSDependency/:TSConfigKey,package.json(incl. dev/peer/optional) + lockfiles,tsconfig.jsonJSONC tolerance,TS_USES_CONFIGcodeanalyzer-java—:JArtifact/:JDependency/:JConfigKey,pom.xml/ Gradle coordinate parsing (net-new),config_useinanalysis.json(Neo4jJ_USES_CONFIGdeferred)python-sdk— v2 Pydantic model mirror (co-evolution rule); facade surface exposure is later, out of scope hereDesign decisions
applicationby containment; sibling maps would introduce a second. This is a strict superset of the source proposal — the two string back-references (dependency→manifest, config→file) become containment edges, artifacts get durablecan://…/@artifact/…ids, and the JSON matches the Neo4j graph the source doc already drew.artifact_kindis a typed closed enum (build_manifest | dependency_lockfile | configuration | deployment_manifest | container | infrastructure | ci | script | documentation | data | other).otheris the catch-all — coverage over classification; a file is never dropped for lack of a parser.--artifact-text/--no-artifact-textflag;path+content_hashalways present so a truncated/binary artifact still dereferences. Emit contract, not schema-versioned.scopeenum + ecosystem mapping table are coined once in the schema, not per-analyzer: runtime | development | test | build | optional | unknown, with the Maven/npm/pypi mapping in the contract.config_use(candidate 18) is scoped in with a per-projection split, not deferred whole. The edge name and its (namespace,key) join are identical across all three languages and both projections.analysis.jsoncarries it for all three; Neo4j carriesPY_USES_CONFIG+TS_USES_CONFIGnow.Obsolete as of 2026-08-29: candidate 9 landed — codeanalyzer-java#203 shipped the canonical v2 graph projection (J_USES_CONFIG(Java's Neo4j config-use edge) is explicitly OUT — Java's Neo4j projector has no body-node label until its v1→v2 migration (candidate 9).V2GraphProjector,V2SchemaCatalog, graph contract2.0.0, since2.1.0), removed the--schema v2 --emit neo4jguard, made v2 the default schema, and codeanalyzer-java#205 followed. Java's projector does have body-node labels now. WhetherJ_USES_CONFIGis in or out is therefore a live decision for whoever picks up codeanalyzer-java#197, not a settled deferral. SDK facade/query surface for the artifact layer is OUT (later). This spec implements no analyzer — that is the codeanalyzer-backend rung, per repo, after the epic exists.Release plan
codeanalyzer-schemalands first — the v2 models,config_useedge,scopetable, Neo4j contract. Gates everything.python-sdkmirrors the v2 models after any ONE analyzer is conformant — it does not wait for all three.Obsolete — that migration has landed (see the scope guard above). Re-decide with codeanalyzer-java#197 rather than inheriting the deferral.J_USES_CONFIGrides Java's candidate-9 v1→v2 Neo4j migration, not this epic.Definition of done (epic-level)
artifact/dependency/config_keynode kinds and theconfig_useedge defined once incodeanalyzer-schemawith identical field names, enum values, and edge payload across python / java / typescript (parity clause holds — no renamed/repurposed shared vocabulary).scopeenum and its ecosystem mapping table are encoded in the schema, not per-analyzer.analysis.jsonround-trips through the schema for all three languages.PY_USES_CONFIGandTS_USES_CONFIGpresent in Neo4j.J_USES_CONFIGis no longer automatically a gap — Java's v2 projector exists as of codeanalyzer-java#203, so either it ships too or its absence is a recorded decision, not an inherited blocker.python-sdkv2 models mirror the three node kinds and the edge.Contract as shipped (python v1.3.0, 2026-08-29) — supersedes the decisions above where they differ
The python train implemented the full layer (issues codellm-devkit/codeanalyzer-python#152, #157, #162, #165; released as 1.3.0 on PyPI). Where implementation experience diverged from this epic's sketch, the shipped contract is the one siblings copy — rationale in the reconciliation and the settled-vocabulary comment below:
Artifact/Package/ConfigKeycarry no language prefix (a Dockerfile or a PyPI package is not a language concept; two analyzers over one repo MERGE onto one node). Language-specific edges keep the prefix:PY_PROVIDES,PY_UNRESOLVED_IMPORT,*_USES_CONFIGper language. This replacesPyArtifact/TSArtifact/JArtifactlabel triplets.can://artifact/<app>/<path>(language-neutral namespace, not…/@artifact/…under the language); packages by purl (pkg:pypi/…,pkg:npm/…,pkg:maven/…); config keys<artifact-id>@key/<dotted.key>. Precondition for joins: agreed--app-name.application.artifactssibling map (config keys nested per artifact) rather than containment-tree nodes — the tree stays code-only.roles: string[]vocabulary (dependency-manifest, service-topology, container-image, ci, env, tool-config, packaging, script, docs, legal, iac, unknown) instead of the closedartifact_kindenum; never-drop still holds (unmatched decodable files →unknown, binaries inventoried hash-only).kind∈ {runtime, dev, optional, build} (in place of thescopeenum),prov∈ {declared, lockfile, installed-metadata, heuristic},direct: falsefor lockfile-only transitives,locked_version,provides_imports.--artifact-text/--no-artifact-text,--artifact-text-max-bytes; sha256/size always full-file; manifests/config parse from full on-disk text so extraction never depends on the stored (possibly truncated) copy.non-literal/undefined-key). Deployment-env sources (DockerfileENV, compose/k8senv) mint bindableenv-namespace keys; DockerfileARGgets a non-bindabledockerfilenamespace.SCHEMA_VERSIONholds at 2.0.0 until the layer settles across analyzers — one bump then.codeanalyzer-schema-repo-first gate did not hold — python shipped in-repo models. Extracting shared models intocodeanalyzer-schemaremains open, now with a working reference to extract from.Reference implementation to copy from: specs
docs/design/specs/2026-08-27-artifacts-and-dependencies-design.md,2026-08-28-config-key-family-design.md,2026-08-28-config-use-edge-design.md(all in codeanalyzer-python); the v1.3.0 release assets include the machine-readable contract (schema.json,schema.cypher) and the query cookbook (analyzing-canpy-graphs-skill.tar.gz).