{
  "name": "Package dependencies",
  "nodes": [
    {
      "id": "n_project",
      "name": "Project",
      "labels": ["Project", "Consumer"],
      "shape": "circle",
      "description": "An application or library that consumes packages. The root of the dependency graph and the only node in it that a developer actually edits by hand — everything below is a consequence of what is declared here.",
      "properties": [
        { "name": "projectId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the project." },
        { "name": "name", "type": "string", "required": true, "indexed": true, "description": "Project name as declared in its manifest." },
        { "name": "ecosystem", "type": "enum", "required": true, "default": "npm", "description": "Which package ecosystem the project draws from: npm, cargo, pypi, maven or go. Determines how version ranges are written and how resolution behaves, and the two differ more than they look." },
        { "name": "manifestPath", "type": "string", "description": "Path to the file that declares the direct dependencies, such as package.json or Cargo.toml." },
        { "name": "isPrivate", "type": "boolean", "default": "true", "description": "True for a project never published to a registry. Private projects can depend on public packages but nothing depends on them, which makes them graph leaves." }
      ]
    },
    {
      "id": "n_lockfile",
      "name": "Lockfile",
      "labels": ["Lockfile"],
      "shape": "circle",
      "description": "A record of exactly which releases a resolution produced, so that the same install can be reproduced later. Separate from the manifest because the manifest states intent as ranges and the lockfile states the outcome as exact versions — conflating them is what makes builds irreproducible.",
      "properties": [
        { "name": "lockfileId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the lockfile." },
        { "name": "path", "type": "string", "required": true, "description": "Path within the project, such as package-lock.json or Cargo.lock." },
        { "name": "formatVersion", "type": "integer", "required": true, "description": "Version of the lockfile format itself, which changes how the same file must be read. npm's version 3 is shaped quite differently from version 1." },
        { "name": "generatedAt", "type": "datetime", "description": "When the lockfile was last written. A lockfile older than the manifest is a warning sign." },
        { "name": "contentHash", "type": "string", "description": "Hash over the resolved set, used to decide whether an install can be skipped." }
      ]
    },
    {
      "id": "n_resolution",
      "name": "Resolution",
      "labels": ["Resolution"],
      "shape": "diamond",
      "description": "One entry in a lockfile: at this position in the tree, this release was chosen. Reified for a reason worth noticing — the same release can appear many times in one lockfile at different paths, so a plain edge from lockfile to release could not tell those occurrences apart. When a pairing is not unique, it needs its own identity.",
      "properties": [
        { "name": "resolutionId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for this entry." },
        { "name": "dependencyPath", "type": "string", "required": true, "description": "Where in the installed tree this copy sits, such as node_modules/a/node_modules/b. The field that makes two copies of one release distinguishable, and the reason nested installs work at all." },
        { "name": "isDirect", "type": "boolean", "required": true, "default": "false", "description": "True where the project asked for this package itself, false where it arrived as somebody else's dependency. The ratio of the two is the honest measure of a project's exposure." },
        { "name": "scope", "type": "enum", "required": true, "default": "runtime", "description": "Why the entry is present: runtime, development, peer, optional or build. Development entries ship to nobody, which changes how seriously a vulnerability in one should be taken." },
        { "name": "integrityHash", "type": "string", "description": "Hash of the artifact actually fetched, checked on install. What stops a registry serving different bytes under the same version." },
        { "name": "resolvedUrl", "type": "string", "description": "Exact URL the artifact came from, which need not be the package's usual registry." }
      ]
    },
    {
      "id": "n_package",
      "name": "Package",
      "labels": ["Package"],
      "shape": "circle",
      "description": "A named unit of code as an identity, independent of any version — 'lodash', not 'lodash 4.17.21'. Keeping the name separate from the versions is the move the whole model rests on: a dependency is declared against this, never against a release.",
      "properties": [
        { "name": "packageId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier. A package name is unique only within its registry, so the name alone cannot identify one." },
        { "name": "name", "type": "string", "required": true, "indexed": true, "description": "Published name, including any scope or namespace, such as @scope/thing." },
        { "name": "normalisedName", "type": "string", "indexed": true, "description": "Name folded to the registry's comparison rules. Some ecosystems treat underscores, hyphens and case as equivalent, so two spellings are one package — and typosquatting lives in the gap between the two forms." },
        { "name": "firstPublishedAt", "type": "datetime", "description": "When the name was first claimed. A very new package with a very familiar name deserves a second look." },
        { "name": "monthlyDownloads", "type": "integer", "description": "Recent download count, cached from the registry rather than computed here. Kept because it is the usual proxy for how much breakage a bad release would cause." },
        { "name": "isNamespaced", "type": "boolean", "default": "false", "description": "True where the name is scoped to an organisation, which constrains who may publish to it." }
      ]
    },
    {
      "id": "n_release",
      "name": "Release",
      "labels": ["Release", "Artifact"],
      "shape": "circle",
      "description": "One published version of a package, and immutable by convention: the bytes behind a version should never change once published. Everything a dependency actually resolves to is a release, but nothing ever depends on one directly.",
      "properties": [
        { "name": "releaseId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier. A version string identifies a release only in combination with its package." },
        { "name": "version", "type": "string", "required": true, "indexed": true, "description": "Version as published, such as 4.17.21. Compared by the ecosystem's own ordering rules, which are not string ordering and not always semantic versioning either." },
        { "name": "publishedAt", "type": "datetime", "required": true, "description": "When the release went out. Ordering by publication and ordering by version disagree whenever an old major line gets a patch." },
        { "name": "isPrerelease", "type": "boolean", "default": "false", "description": "True for versions like 2.0.0-beta.1, which most range operators deliberately exclude unless asked for by name." },
        { "name": "isYanked", "type": "boolean", "required": true, "default": "false", "description": "True where the release has been withdrawn. Yanked releases stay in the graph because lockfiles still point at them, and pretending they are gone breaks the historical record rather than fixing anything." },
        { "name": "integritySha512", "type": "string", "description": "Publisher-declared hash of the artifact." },
        { "name": "hasProvenanceAttestation", "type": "boolean", "default": "false", "description": "Whether the release carries a signed statement tying it to the source commit and build that produced it. The main defence against a compromised publishing account." }
      ]
    },
    {
      "id": "n_registry",
      "name": "Registry",
      "labels": ["Registry"],
      "shape": "square",
      "description": "The service that hosts packages and serves their artifacts. Reference data, and the trust anchor for everything below it: a package's identity is only unique with respect to its registry.",
      "properties": [
        { "name": "registryCode", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "Short stable code for the registry, such as npmjs or crates-io." },
        { "name": "name", "type": "string", "required": true, "description": "Name of the registry service." },
        { "name": "url", "type": "string", "description": "Base URL artifacts are fetched from." },
        { "name": "allowsUnpublish", "type": "boolean", "default": "false", "description": "Whether a publisher may withdraw a release after the fact. A registry that allows it can break every build that depends on the removed version, which has happened and is why most registries no longer do." },
        { "name": "requiresTwoFactorToPublish", "type": "boolean", "default": "false", "description": "Whether publishing demands a second factor. The single most effective control against account takeover in the supply chain." }
      ]
    },
    {
      "id": "n_repository",
      "name": "Repository",
      "labels": ["Repository"],
      "shape": "circle",
      "description": "The source repository a package is developed in. Linked because provenance questions are answered here rather than at the registry, and because an archived or deleted repository behind a widely used package is a maintenance risk long before it is a security one.",
      "properties": [
        { "name": "repositoryId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the repository." },
        { "name": "url", "type": "string", "required": true, "unique": true, "indexed": true, "description": "Canonical clone URL. Declared by the publisher and not verified by most registries, so it is a claim rather than a fact unless provenance backs it." },
        { "name": "host", "type": "enum", "required": true, "default": "github", "description": "Where the repository is hosted: github, gitlab, bitbucket, self_hosted or other." },
        { "name": "defaultBranch", "type": "string", "description": "Branch releases are normally cut from." },
        { "name": "isArchived", "type": "boolean", "default": "false", "description": "True where development has formally stopped. An archived repository under a heavily depended-on package is the clearest available signal that nobody will fix the next bug." }
      ]
    },
    {
      "id": "n_maintainer",
      "name": "Maintainer",
      "labels": ["Maintainer", "Person"],
      "shape": "circle",
      "description": "An account permitted to publish releases of a package. Modelled because the supply-chain question is rarely 'is this code safe' and usually 'who can change it tomorrow'.",
      "properties": [
        { "name": "maintainerId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the account." },
        { "name": "username", "type": "string", "required": true, "unique": true, "indexed": true, "description": "Registry account name. Unique on that registry, and reusable after deletion on some — which is a known hijacking route." },
        { "name": "displayName", "type": "string", "description": "Name shown on the registry profile." },
        { "name": "email", "type": "string", "description": "Contact address on the account. Where it is an expired domain, the account can be taken over by re-registering it." },
        { "name": "hasTwoFactorEnabled", "type": "boolean", "default": "false", "description": "Whether the account is protected by a second factor. Meaningful only in combination with the registry's own policy." },
        { "name": "accountCreatedAt", "type": "datetime", "description": "When the account was opened. A new account publishing to a long-established package is worth an alert." }
      ]
    },
    {
      "id": "n_license",
      "name": "License",
      "labels": ["License"],
      "shape": "square",
      "description": "A licence under which a release is offered, identified by its SPDX code. Reference data shared across the whole graph, and attached to releases rather than packages because licences are relicensed between versions more often than people expect.",
      "properties": [
        { "name": "spdxIdentifier", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "SPDX short identifier, such as MIT or Apache-2.0. The whole point of SPDX is that this string means exactly one thing, which is why it can be the key." },
        { "name": "name", "type": "string", "required": true, "description": "Full name of the licence." },
        { "name": "isOsiApproved", "type": "boolean", "default": "false", "description": "Whether the Open Source Initiative has approved it. The usual dividing line in corporate policy." },
        { "name": "isCopyleft", "type": "boolean", "default": "false", "description": "Whether the licence propagates its terms to derived work. The property that makes a transitive dependency a legal question rather than a technical one." },
        { "name": "isDeprecatedIdentifier", "type": "boolean", "default": "false", "description": "True where SPDX has superseded this identifier. Old manifests are full of them and they still have to resolve." }
      ]
    },
    {
      "id": "n_vulnerability",
      "name": "Vulnerability",
      "labels": ["Vulnerability"],
      "shape": "circle",
      "description": "An underlying flaw, independent of who wrote it up. Separate from the advisories that report it because one flaw routinely has a CVE record, a GitHub record and an ecosystem-specific record, all describing the same thing under different identifiers — and counting those as three vulnerabilities is a common and misleading error.",
      "properties": [
        { "name": "vulnerabilityId", "type": "uuid", "key": true, "required": true, "unique": true, "indexed": true, "description": "Surrogate identifier for the flaw itself. Deliberately not a CVE number, because a CVE is one publisher's record of it." },
        { "name": "summary", "type": "string", "required": true, "description": "Short statement of what the flaw allows." },
        { "name": "severity", "type": "enum", "required": true, "default": "moderate", "description": "Assessed severity: low, moderate, high or critical. An assessment rather than a measurement, and different publishers frequently disagree about the same flaw." },
        { "name": "cvssScore", "type": "float", "description": "Numeric severity score, where one has been assigned." },
        { "name": "cvssVector", "type": "string", "description": "The vector string the score was derived from. Worth storing because the score alone hides whether the flaw is remotely reachable." },
        { "name": "cweIdentifier", "type": "string", "description": "Weakness class, such as CWE-79. Groups flaws by kind rather than by package." },
        { "name": "disclosedOn", "type": "date", "description": "Date the flaw became public, which starts the clock everyone is measured against." }
      ]
    },
    {
      "id": "n_advisory",
      "name": "Advisory",
      "labels": ["Advisory"],
      "shape": "circle",
      "description": "One publisher's record of a vulnerability. Exists as its own node so that aliases resolve: GHSA, CVE and ecosystem databases each issue an identifier, and a tool that treats them as distinct findings reports the same problem three times.",
      "properties": [
        { "name": "advisoryIdentifier", "type": "string", "key": true, "required": true, "unique": true, "indexed": true, "description": "The published identifier, such as GHSA-jf85-cpcp-j695 or CVE-2019-10744. Globally unique within its scheme and the reference everyone quotes." },
        { "name": "source", "type": "enum", "required": true, "default": "ghsa", "description": "Which database issued it: ghsa, cve, rustsec, pysec or osv. Determines how quickly it appears and how much analysis it carries." },
        { "name": "publishedOn", "type": "date", "required": true, "description": "Date this record was published, which can lag the disclosure by weeks." },
        { "name": "withdrawnOn", "type": "date", "description": "Date the record was retracted, if it was. Withdrawn advisories are kept because tools cached them and still act on them." },
        { "name": "url", "type": "string", "description": "Where the record can be read in full." }
      ]
    }
  ],
  "relationships": [
    {
      "type": "REQUIRES", "from": "n_release", "to": "n_package", "cardinality": "n-n",
      "description": "A dependency as it is actually declared, and the edge this model exists to get right. It runs from a release to a *package*, never to another release, because what a publisher writes down is a name and an acceptable range — not a version. Point this at a release instead and you have asserted that only one version can ever satisfy it, which is precisely the assumption that makes two dependants needing different versions unrepresentable.",
      "properties": [
        { "name": "versionRange", "type": "string", "required": true, "description": "The acceptable range, such as ^4.17.0 or >=1.2,<2. The property the whole design turns on: it belongs to this dependency, not to the package, because two dependants ask for different ranges of the same package all the time." },
        { "name": "scope", "type": "enum", "required": true, "default": "runtime", "description": "When the dependency is needed: runtime, development, peer, optional or build. Peer is the awkward one — it asks the consumer to supply the dependency rather than resolving it here." },
        { "name": "isOptional", "type": "boolean", "default": "false", "description": "True where installation may fail without failing the build, as for platform-specific extras." },
        { "name": "platformConstraint", "type": "string", "description": "Operating system or architecture the dependency applies to, where it is conditional. Blank for the usual case." }
      ]
    },
    {
      "type": "DEPENDS_ON", "from": "n_project", "to": "n_package", "cardinality": "n-n",
      "description": "The project's own direct dependencies, declared in its manifest. The same relation as REQUIRES from a different kind of consumer, and worth keeping distinct because the direct set is the only part a developer chose deliberately.",
      "properties": [
        { "name": "versionRange", "type": "string", "required": true, "description": "Range the project will accept, written by hand and therefore the place where over-permissive ranges enter the graph." },
        { "name": "scope", "type": "enum", "required": true, "default": "runtime", "description": "Whether the dependency ships: runtime, development, peer, optional or build." }
      ]
    },
    {
      "type": "VERSION_OF", "from": "n_release", "to": "n_package", "cardinality": "n-1",
      "description": "Ties a published version back to the name it was published under. Many-to-one, and the only route from a release to its package — which is what makes a dependency on a name resolvable to a set of candidate releases."
    },
    {
      "type": "HAS_LOCKFILE", "from": "n_project", "to": "n_lockfile", "cardinality": "1-n",
      "description": "The lockfiles a project maintains. One-to-many because a polyglot project or a monorepo workspace carries several, and they resolve independently of each other."
    },
    {
      "type": "PINS", "from": "n_lockfile", "to": "n_resolution", "cardinality": "1-n",
      "description": "The entries a lockfile contains. One-to-many and typically very many: a modest project's lockfile holds thousands, which is the honest picture of what was installed."
    },
    {
      "type": "RESOLVES_TO", "from": "n_resolution", "to": "n_release", "cardinality": "n-1",
      "description": "The concrete release a lockfile entry selected. This is where a range finally becomes a version — REQUIRES states what would be acceptable, RESOLVES_TO records what was actually chosen, and keeping the two apart is what makes a lockfile meaningful."
    },
    {
      "type": "SUPERSEDES", "from": "n_release", "to": "n_release", "cardinality": "n-n",
      "description": "A release published to replace an earlier one on the same line. Self-referencing and deliberately **directional**: a fix supersedes the flaw it repairs and never the reverse. Worth comparing with CONFLICTS_WITH below, which is on the same node type and is symmetric — the two together are the clearest illustration of when a relationship should carry direction and when it should not.",
      "properties": [
        { "name": "reason", "type": "enum", "required": true, "default": "newer_release", "description": "Why the replacement was published: newer_release, security_fix or yanked_replacement. Only some of these oblige a consumer to move." },
        { "name": "isSecurityBackport", "type": "boolean", "default": "false", "description": "True where the fix was applied to an older major line rather than only to the newest. The case that breaks any assumption that later publication means higher version." }
      ]
    },
    {
      "type": "CONFLICTS_WITH", "from": "n_release", "to": "n_release", "cardinality": "n-n", "bidirectional": true,
      "description": "Two releases that must not be installed together — the pattern Debian records in its Conflicts field. Marked bidirectional because the statement is genuinely symmetric: if A cannot coexist with B then B cannot coexist with A, and there is no sense in which one of them is the source. Writing it as two opposing edges would mean two records to maintain and nothing keeping them in step.",
      "properties": [
        { "name": "reason", "type": "string", "required": true, "description": "Why the two cannot coexist — a shared file, an incompatible native binding, a global registration only one may hold." },
        { "name": "detectedBy", "type": "enum", "required": true, "default": "registry_metadata", "description": "How the conflict is known: registry_metadata, maintainer_declaration or observed_failure. The last is the common one, and the least reliable." }
      ]
    },
    {
      "type": "LICENSED_UNDER", "from": "n_release", "to": "n_license", "cardinality": "n-n",
      "description": "The licences a release is offered under. Many-to-many because dual licensing is ordinary — an SPDX expression such as MIT OR Apache-2.0 gives the consumer a choice, while AND obliges them to satisfy both.",
      "properties": [
        { "name": "expressionOperator", "type": "enum", "required": true, "default": "single", "description": "How this licence combines with the others on the same release: single, and, or. The difference between a choice and an obligation, and the reason a flat list of licence names is not enough." }
      ]
    },
    {
      "type": "HOSTED_ON", "from": "n_package", "to": "n_registry", "cardinality": "n-1",
      "description": "The registry a package name lives on. Many-to-one, and it is what completes the package's identity — the same name on two registries is two different packages, a fact that dependency-confusion attacks exist to exploit."
    },
    {
      "type": "DEVELOPED_IN", "from": "n_package", "to": "n_repository", "cardinality": "n-1",
      "description": "The source repository the package is built from, as declared by its publisher. Many-to-one because a monorepo publishes many packages from one repository."
    },
    {
      "type": "MAINTAINS", "from": "n_maintainer", "to": "n_package", "cardinality": "n-n",
      "description": "Permission to publish new releases of a package. Many-to-many, and the edge to walk when asking who could change what: a single maintainer account with rights over several widely used packages is a concentration of risk that no amount of code review addresses."
    },
    {
      "type": "PUBLISHED_BY", "from": "n_release", "to": "n_maintainer", "cardinality": "n-1",
      "description": "The account that actually pushed this particular release. Distinct from who maintains the package, and the distinction is the point — a release published by an account that has never published before is the signature of a compromised credential."
    },
    {
      "type": "AFFECTS", "from": "n_vulnerability", "to": "n_package", "cardinality": "n-n",
      "description": "The packages a flaw is present in. Points at the package and carries a range, exactly as a dependency does — advisories describe affected versions as ranges, not as lists of releases, and for the same reason: the set of matching releases is derived, never enumerated.",
      "properties": [
        { "name": "affectedRange", "type": "string", "required": true, "description": "Versions the flaw is present in, in the ecosystem's own range syntax. The mirror image of a dependency's versionRange, and evaluated the same way." },
        { "name": "introducedInVersion", "type": "string", "description": "First version carrying the flaw, where it is known. Frequently is not." },
        { "name": "fixedInVersion", "type": "string", "description": "First version that no longer carries it. Blank where no fix has shipped, which is the case worth alerting on." },
        { "name": "ecosystemSpecific", "type": "string", "description": "Qualifications that only make sense in one ecosystem, such as an affected build feature or platform." }
      ]
    },
    {
      "type": "REPORTS", "from": "n_advisory", "to": "n_vulnerability", "cardinality": "n-1",
      "description": "The flaw an advisory describes. Many-to-one, and that is the whole reason both nodes exist: several published records commonly point at one underlying vulnerability, and collapsing them would either invent duplicates or discard identifiers that tools depend on."
    }
  ]
}
