Progress Tracker — HDL IP Packager¶
Rule: Track only what is actionable now. Sections: - Roadmap — the ordered plan for the project, newest milestones first to build. - Blocking Issues — must-fix before the next release. - Open Non-Blocking Issues — known backlog worth doing, not gating a release. - Backlog (deferred) — consciously parked; revisit only if the trade-off changes. - Completed Milestones — finished work, newest at the top (changelog source). - Archive — entries older than ~6 months and no longer actionable.
Add new entries at the top of the relevant section. Do not keep an "In Progress" list — work in progress lives on the branch. Never delete entries; move them to Archive. Convert relative dates to absolute (e.g. "June 2026").
Current Status — June 2026¶
Active branch: main
Version: 0.14.0 cut — Git-registry hardening + richer IP-XACT (see
docs/design/0.14.0-git-and-ipxact.md). Workstream A:
the four GitRegistry fixes (tag-preferred ref resolution, offline @sha/gen --locked,
scp-URL rejection), validated end to end in hdlpkg-livetest against a real remote.
Workstream B: an IP-XACT 2022 output mode (export-ipxact --std {2014,2022},
validated against the official 1685-2022 XSD) and parameter mapping via an optional,
ignorable [ipxact.parameters] manifest section. No ip.toml/ip.lock/CLI break — the
[ipxact.parameters] table is purely additive (no schema bump) and the --std flag is
optional. The prior release (0.13.0) was the hdl-ip-packager -> hdlpkg rename.
Next: 0.15.0+ — supply-chain (Sigstore signing) + confidential IP (IEEE 1735), and the
deferred IP-XACT ports/bus-interface mapping, as the backlog matures. The project stays
pre-1.0 (formats keep the licence to change), validated continuously via
hdlpkg-livetest; 1.0.0 is a deliberate freeze for later. See the Release plan.
Stage: Feature-complete for the roadmap (M1–M8) and iterating through the backlog as
0.x capability releases; fully typed, linted, and tested (509 passing tests, ~95% coverage):
- Versioning — SemVer 2.0.0 Version + VersionConstraint (caret/tilde/range
grammar, pre-release precedence).
- Identity — PackageRef and Vlnv (vendor:library:name:version).
- Manifest — ip.toml parsing/validation ([package], [dependencies],
[filesets], [targets]), with an optional schema version for a migration path.
- Resolver — backtracking, newest-compatible dependency resolution that unifies
SemVer-compatible dependents (Cargo-style) and applies a configurable
[resolution] on-conflict policy to an incompatible conflict
(fail_on_conflict/use_latest/isolate_namespaces, the last keeping multiple
versions per package); scheme-aware (semver/opaque); pure, fed by an in-memory
version index.
- Lockfile — deterministic ip.lock (serialize/parse/verify a Resolution
with per-core source + SHA-256), written by hdlpkg resolve.
- Cache — content-addressed local blob store (SHA-256 key, verify-on-read,
atomic writes), populated by hdlpkg install.
- Registry — Registry interface + local-dir/writable-local/HTTP/OCI backends
behind one registry_from_location scheme dispatch (path / http(s):// / oci://),
all writable (append-only + yank), + a dependency-graph walker feeding the resolver
(Git backend is an open Non-Blocking issue).
- Credentials — per-host Credential (direct bearer or username+secret) for private
registries via hdlpkg login; OCI token-exchange (401 -> realm -> scoped access token);
~/.docker/config.json reused as a fallback.
- Packaging — deterministic .ipkg artifact (pack_core/extract_ipkg); the
packed-content digest is what the cache and lockfile pin.
- Backends — tool-flow generation (backends/): a pure EDAM-like intermediate
(build_eda_design, honoring Fileset.depend) feeding Verilator, Vivado, Icarus,
GHDL, and Yosys backends behind shared, hardened guards.
- IP-XACT — IEEE 1685-2014 component export (ipxact.py: to_ipxact) behind
hdlpkg export-ipxact.
- Supply-chain — content checksums (SHA-256) everywhere, plus a deterministic
CycloneDX SBOM (sbom.py: build_cyclonedx) emitted by hdlpkg pack --sbom
(Sigstore signing deferred).
- CLI — all commands implemented: info/validate/init/add/resolve/
install/pack/publish/pull/yank/login/logout/gen/tree/export-ipxact.
install --locked and gen --locked give reproducible, lockfile-driven builds.
- Tooling — pytest (markers + coverage gate + foldable summary), ruff, mypy
strict on src/, CI workflow, and a cross-platform test-summary renderer.
Next: all roadmap milestones (M1–M8) are delivered and the registry/OCI protocol is
implemented — local, HTTP, OCI, and Git backends behind one registry_from_location
abstraction, with hdlpkg login auth (direct bearer + OCI token-exchange + docker login
reuse). Development now continues as a series of ordered 0.x capability releases working
through the Open Non-Blocking Issues: next up (0.12.0) an HDL-aware frontend (parser) +
module/entity multi-version coexistence; then (0.13.0) Git-registry hardening — exercised
end to end via hdlpkg-livetest — and richer IP-XACT (bus interfaces, parameters) + IP-XACT
2022; then supply-chain (Sigstore signing) and confidential-IP (IEEE 1735) work. The project
stays pre-1.0 — the formats keep the licence to change — with 1.0.0 reserved as a
deliberate freeze once the design has settled.
Roadmap (ordered — build top-down)¶
Each milestone should land with tests and a docs update. The design for every item is in architecture.md; the rationale is in research/state_of_the_art.md.
All roadmap milestones (M1–M8) are delivered. Development now continues as ordered 0.x
capability releases that work through the Open Non-Blocking Issues below — the HDL-aware
frontend + module/entity coexistence, Git-registry hardening, richer IP-XACT, Sigstore
signing, and encrypted-IP carry. The project stays pre-1.0 so the formats keep the licence to
iterate; 1.0.0 is a deliberate freeze for later (see the Release plan), not a tracked
near-term gate.
Release plan¶
The packager is pre-1.0 and stays there for now: it ships a steady stream of
0.MINOR.PATCH capability releases that work through the open issues, keeping the SemVer
0.x licence to change ip.toml / ip.lock / the CLI when a feature needs it.
- MINOR (0.11 -> 0.12) = a capability release; may carry ip.toml / ip.lock / CLI
changes (the 0.x licence to iterate).
- PATCH (0.12.0 -> 0.12.1) = bug / doc fixes; no new capability, no format break.
- X.Y.Z-rc.N pre-release tags stay available for anything risky (version.py handles
precedence; release.yml accepts them), but are not used to gate a long candidate
period — see validation below.
The compatibility contract SemVer tracks here is the on-disk formats users commit to their
repos (ip.toml, ip.lock) plus the hdlpkg CLI surface — more than the Python API. Cut a
release at each point a user can do something new end to end.
Validation is continuous, not a one-shot release candidate. The
hdlpkg-livetest project and the
hdlpkg-consumer-demo exercise install / publish / consume (and the real-toolchain build
lane) against live registries on every meaningful change, so confidence accrues release by
release rather than through a single frozen candidate.
Shipped so far (details in Completed Milestones): 0.1.0–0.6.0 delivered M1–M7;
0.7.0/0.8.0 the supply-chain + pre-1.0 completeness; 0.9.0 the versioning contract +
local/HTTP/OCI registries + auth; 0.10.0 the man page; 0.11.0 the Git registry,
registry-driven gen, and IP-XACT XSD validation.
Planned next (ordered; each a 0.x.0):
| Version | Focus | What it adds |
|---|---|---|
| 0.12.0 | HDL-aware frontend | Source-unit tokenizing + a real HDL parser, and module/entity multi-version coexistence built on it (beyond package mangling). The heaviest item on the roadmap — may split across two releases. |
| 0.13.0 | Git registry + IP-XACT depth | Harden the Git backend (the ref-resolution/parsing follow-ups) and exercise git+ registries end to end via hdlpkg-livetest; richer IP-XACT mapping (bus interfaces, parameters) + an IP-XACT 2022 output mode. |
| 0.14.0+ | Supply-chain + confidentiality | Sigstore (cosign) signing of .ipkg + SBOM; encrypted-IP carry (IEEE 1735); remaining backlog as it matures. |
| 1.0.0 | The stability freeze — later | A deliberate commitment that ip.toml / ip.lock / CLI / registry protocol stop moving, made once they have held stable across several 0.x releases and real hdlpkg-livetest + adopter use. Not a date, not gated by a long candidate period. |
1.0.0 is a promise that the formats and interfaces freeze — taken when the design has
settled, after the parser/coexistence and IP-XACT work above land and the formats prove
stable in practice. Until then the project stays 0.x so it can still improve the formats
without breaking a stability promise.
Blocking Issues (must fix before the next release)¶
None. Both blockers the 0.10.0 third-party trial surfaced (against a JFrog
Artifactory behind a Cloudflare tunnel) are fixed and in Completed Milestones: the
Cloudflare User-Agent rejection (#12)
and gen being unable to consume installed/cached/published deps
(#13). Both were
additive/internal (no ip.toml/ip.lock format change). There are no open blockers for
the next 0.x release.
Open Non-Blocking Issues¶
| Issue | File | Notes |
|---|---|---|
| Encrypted IP distribution (IEEE 1735) | packaging.py, registry.py, manifest.py, cli.py |
Future feature. Let a producer distribute a core whose HDL source is encrypted, so a consumer can resolve/install/gen against it (the tool can drive a tool flow) without the source ever being readable on disk. Two distinct layers, decide which to build: (a) Standard HDL IP encryption (IEEE 1735 / pragma protect) — the cross-vendor norm. Each source file carries an encrypted envelope (a symmetric session key wrapped under each tool vendor's public key + AES/RSA-encrypted payload, IEEE 1735 v1/v2 with "rights" digests). The EDA tool decrypts at compile time; the packager's job is to carry, not break these envelopes — pack/extract/SBOM must treat an encrypted file as opaque, the deterministic-pack digest still pins ciphertext, and gen must not assume it can read the source. The tool would not implement the crypto itself (vendor keys live in the EDA tools); at most it could shell out to vivado -encrypt/vlog +protect to produce envelopes. (b) At-rest/transport encryption of the .ipkg — encrypt the whole artifact in the registry/cache for confidential distribution (e.g. age/GPG or an OCI-layer key), decrypted on pull with a consumer key. This is independent of HDL-tool semantics and simpler, but does not give the per-tool, compile-time protection (a) does. Open questions: where keys/recipients are declared (a [package]/[encryption] manifest key vs. out-of-band), how it interacts with content-addressing (the digest must pin what is stored), how the SBOM marks a component encrypted, and how validate/info behave when source is unreadable. Needs a real EDA tool (or an interop fixture) to test (a) honestly — defer like the Git/OCI/Sigstore work. |
| Sigstore (cosign) artifact signing | packaging.py, .github/workflows/ |
The unbuilt half of M8: keyless signing of the .ipkg + SBOM and a verify path. Needs OIDC + Fulcio/Rekor (or a managed key) and a live transparency log to implement and test honestly — deferred like the Git backend. Checksums + SBOM already ship; this adds authenticity on top. |
| Richer IP-XACT mapping: ports + bus interfaces | ipxact.py, manifest.py, tests |
Parameters and the IP-XACT 2022 output mode shipped in 0.14.0 (see Completed Milestones): export validates against both the official 1685-2014 and 1685-2022 XSDs, and an optional [ipxact.parameters] section maps component parameters. Remaining, optional: map ports and bus interfaces (and memory maps) via the same additive [ipxact.*] manifest approach (decision B-opt1) — deferred until a real consumer needs them (not the current Vivado-ingest use case). |
| Name-mangling — named-library references + classifier cleanup | mangle.py |
Follow-ups filed from the 0.12.0 release review (not release blockers). (1) Named-library references to a coexisting unit are left untouched: a use mylib.bus / entity mylib.foo reference to a colliding package/entity is not rewritten while its declaration is, so a design that references a coexisting unit via a named library (not work) would dangle. Unreachable in the current single-work-library gen flow (everything is analyzed into work), and the same long-standing limitation applies to packages; the safe fix is to refuse on a named-lib reference to a colliding unit (parity with the SV classifiers). (2) Cleanup: _reject_unclassifiable_sv_modules / _reject_unclassifiable_sv_interfaces and the per-kind declaration scanners share structure — collapse into one parameterized helper so the scan scaffold cannot drift between kinds. |
| Git-backed registry — ref-resolution and parsing hardening | registry.py |
Fixed (June 2026, 0.14.0 workstream A — see Completed Milestones). All four 0.11.0-review follow-ups landed: (1) _resolve now prefers the immutable tag over a same-named remote branch (refs/tags/<ref> -> origin/<ref> -> raw SHA); (2) scp-style URLs (git+git@host:repo.git) are rejected in _parse_git_location with a hint to use git+ssh://; (3) a bare scp URL (git@host:repo.git) is detected in registry_from_location and hinted (no longer a confusing LocalRegistry miss); (4) _sync short-circuits the fetch when the pinned @<sha> is already present in the clone (true offline-after-install). Plus two follow-on fixes the offline guarantee needed: the clone cache is keyed by repo URL (so an @sha pin reuses a no-ref clone) and gen --locked skips building the registry when the cache is complete. Unit-tested for the pure parse/decision logic; the real-git-server cases (tag-vs-branch, offline) run as integration tests (skip on the CFA-restricted Windows box, run in CI) and are end-to-end validated in hdlpkg-livetest (new --git / --git-remote modes, passed against a live remote). None were integrity bugs (the gen --locked digest check fails closed regardless). |
Backlog (deferred — low value / not currently planned)¶
| Issue | Why parked |
|---|---|
Switch build backend setuptools→uv/hatch workflow tooling |
hatchling backend already works and is PEP-compliant; revisit only if the team standardizes on uv end to end. |
| Source-unit tokenizing (auto-discover HDL deps like Orbit) | Powerful but large; only worth it after the manifest-driven flow (M1–M5) is solid. |
Mutation testing (mutmut) |
Validates test quality, but slow and only worth it once the implemented surface is larger. |
Completed Milestones¶
Release 0.14.0 — Git-registry hardening + richer IP-XACT — June 2026¶
- [x] Cut
0.14.0, bundling the two workstreams of docs/design/0.14.0-git-and-ipxact.md: (A) Git-registry hardening — tag-preferred ref resolution, the offline@sha/gen --lockedguarantee (clone cache keyed by URL; lazy registry when the cache is complete), and scp-URL rejection — validated end to end inhdlpkg-livetestagainst a real remote; and (B) an IP-XACT 2022 output mode (export-ipxact --std {2014,2022}, validated against the official 1685-2022 XSD, vendored) plus parameter mapping via an optional[ipxact.parameters]manifest section. Noip.toml/ip.lock/CLI break —[ipxact.parameters]is additive and ignorable (noschemabump) and--stdis optional. Bumpedpyproject.toml+src/hdlpkg/__init__.pyto0.14.0; regenerated the man page. Next:0.15.0+ (Sigstore signing, confidential IP, deferred IP-XACT ports/bus interfaces).
0.14.0 workstream B (part 2) — IP-XACT parameter mapping — June 2026¶
- [x]
export-ipxactnow maps producer-declared parameters, the richer-mapping half of workstream B (design decision 4: parameters now, ports/bus interfaces deferred). A new optional[ipxact.parameters]manifest table (Manifest.ipxact_parameters, a tuple of the newIpxactParameter) accepts a scalar (WIDTH = 8, stringified — bools astrue/false) or a{ value, description }table;to_ipxactemits anipxact:parametersblock (name + optional description + value) at the end of the component, XSD-valid in both 2014 and 2022. Purely additive and ignorable — an older hdlpkg skips the[ipxact]table — so noschemabump; unknown[ipxact.*]subsections and unknown per-parameter keys are rejected (no silent drops). Files:manifest.py(IpxactParameter,_parse_ipxact),ipxact.py,__init__.py(exportIpxactParameter),tests/unit/test_manifest.py,test_ipxact.py,test_ipxact_xsd.py. This closes the 0.14.0 IP-XACT scope; the remaining ports/bus-interface mapping stays an Open Non-Blocking item. 0.14.0 is now feature-complete (workstreams A + B); next: cut the release.
0.14.0 workstream B (part 1) — IP-XACT 2022 output mode — June 2026¶
- [x]
export-ipxactcan now emit IEEE 1685-2022 as well as 1685-2014, via a new--std {2014,2022}flag (default 2014 for back-compat).to_ipxact(manifest, std=...)(ipxact.py) is parameterized by standard: the VLNV / model views+instantiations / fileSets shape is shared; the only structural delta isdescriptionplacement (2022 carries it in thedocumentNameGroupright after the version; 2014 trails it afterfileSets). ThefileTypeuser-escape is unchanged (2022 only adds enum types, so a 2014-valid type is valid in 2022). The full official 1685-2022 XSD set (Apache-2.0) is vendored undertests/schema/ipxact-1685-2022/(test fixture only, not in the wheel), andtests/unit/test_ipxact_xsd.pynow validates both standards against their official XSDs over the example cores + customfileType/description cases, plus a negative test per standard. Additive/internal — noip.toml/ip.lock/format change. Files:ipxact.py,cli.py(+ regeneratedman/hdlpkg.1),tests/unit/test_ipxact.py,tests/unit/test_ipxact_xsd.py,tests/integration/test_ipxact_cli.py,tests/schema/ipxact-1685-2022/. Next (part 2): richer mapping — an optionalip.toml[ipxact.*]section exporting parameters in both standards.
0.14.0 workstream A — Git-registry hardening (the four ref/parse fixes) — June 2026¶
- [x] Fixed all four
GitRegistryfollow-ups filed from the 0.11.0/0.12.0 reviews (workstream A of docs/design/0.14.0-git-and-ipxact.md), hardening provenance and the offline story without anyip.toml/ip.lock/CLI change: - A1 — tag preferred over a same-named branch.
_resolvenow triesrefs/tags/<ref>first, thenorigin/<ref>, then a raw SHA, so a pinnedgit+<url>@v1.0.0binds to the immutable tag rather than the movingorigin/v1.0.0branch tip — closing the provenance-drift gap. - A4 — exact-SHA pins resolve offline.
_syncskips the networkgit fetchwhen the pinned@<sha>(the form a lockfile records) is already a commit in the clone (_commit_present), sogen --lockedagainst agit+source genuinely works offline after the first install. A branch/tag pin (or no pin) still fetches. - A2/A3 — scp-style URLs rejected with a hint.
_parse_git_locationrejectsgit+git@host:repo.git[@ref](its first@is ambiguous), andregistry_from_locationdetects a baregit@host:repo.git(which used to fall through toLocalRegistrywith a confusing "not in the local registry" error) — both point at the supportedgit+ssh://host/path/repo.git[@ref]form. New pure helpers_is_scp_like/_is_full_sha. - Two follow-on fixes the live A4 validation required: (i) the git clone cache is now
keyed by the repo URL (not the full
@reflocation) so an exact-@shapin reuses the clone a no-refinstallalready made; and (ii)gen --lockedno longer constructs the registry at all when every locked artifact is already in the content-addressed cache, so agit+(or any) source is never re-contacted offline-after-install (cli.py:_cmd_gen/_dependency_sourcenow take a nullable registry). - Tests: pure parse/decision logic is unit-tested (
tests/unit/test_registry_location.py— scp rejection, full-SHA detection); the real-git-server behaviours (tag-vs-branch preference, offline@share-resolve after the remote is removed) are integration tests (tests/integration/test_git_registry_cli.py) that skip on the CFA-restricted Windows box but run in CI; the lazy-registry offline-after-install guarantee has a git-free regression test (tests/integration/test_gen_registry_cli.py). - End-to-end validated via
hdlpkg-livetest(passed against a live remote): new--git(local bare repo, fully offline) and--git-remote(a real private GitHub repo, created/reused viagh) modes inrun_livetest.pyassert commit provenance, tag>branch (@shared), digest stability, and the offline@sha+gen --lockedguarantees. Files:src/hdlpkg/registry.py,src/hdlpkg/cli.py, the test modules;hdlpkg-livetest. Next in 0.14.0: workstream B (IP-XACT 2022 output mode + parameter mapping).
Release 0.13.0 — the hdl-ip-packager -> hdlpkg rename — June 2026¶
- [x] Cut
0.13.0: renamed the import packagehdl_ip_packager->hdlpkg(movedsrc/hdl_ip_packager/->src/hdlpkg/) and the PyPI distributionhdl-ip-packager->hdlpkg, taking the name the CLI already used; renamed the GitHub repo and updated the sibling repos (hdlpkg-consumer-demo,hdlpkg-livetest). Noip.toml/ip.lock/CLI or format change — onlyimport hdlpkg/pip install hdlpkgdiffer. Clean break on the import name (nohdl_ip_packageralias); ahdl-ip-packagerdeprecation shim keeps the oldpip installresolving tohdlpkg. Bumpedpyproject.toml+src/hdlpkg/__init__.pyto0.13.0; regenerated the man page. Plan: docs/design/rename-to-hdlpkg.md. Next:0.14.0(Git-registry hardening + richer IP-XACT).
Release 0.12.0 — June 2026¶
- [x] Cut
0.12.0, an additive/internal release (noip.toml/ip.lock/CLI/registry break):gennow name-mangles coexisting modules, interfaces, and entities (not just packages) underisolate_namespaces, including generate-nested instantiations, via the in-house scoped rewriter (approach A, zero new deps). Bumpedpyproject.toml+src/hdl_ip_packager/__init__.pyto0.12.0and regenerated the man page. Proven by the unit/integration suites and the consumer demo'ssoc_modverexample built in real Verilator on CI. Next:0.13.0(Git-registry hardening - richer IP-XACT).
Module/interface/entity multi-version coexistence — June 2026¶
- [x]
gennow name-mangles coexisting modules, interfaces, and entities, not just packages (under[resolution] on-conflict = "isolate_namespaces"). Extended the puremangle.py(approach A — in-house, zero new dependencies) with kind-aware rewrite dispatch and declaration + reference position rules per unit kind: SV modules/programs (instantiation incl.#(…)parameter maps, instance arrays, multiple instances, and generate-nested instances), SV interfaces (also as a port/virtualtype and modport select), and VHDL entities (entity/architecture/componentdeclarations, directentity work.Xand component instantiation, generate-nested). Safety: classify-all-or-refuse for SV modules/interfaces (rename only when every occurrence of a name is provably a declaration, instantiation/reference, or inert — otherwise refuse, never a partial rewrite), plus a cross-ref guard (refuse a colliding name also declared by an unrelated core). Noip.toml/ip.lock/CLI/format change — purely thegenmangling pass; theisolate_namespacesmessaging was updated accordingly. Landed in staged commits (kind-aware refactor → SV modules → VHDL entities → SV interfaces → messaging+integration → demo). Proven end to end bytests/unit/test_mangle.py,tests/integration/test_mangle_unit_gen_cli.py, and the consumer demo's newsoc_modverexample (twowidgetmodule versions + a generate-nested instance) built in real Verilator on CI. Design record: docs/design/module-entity-coexistence.md. Remaining refusals (documented limitations): macro-pasted instantiations, an SV interface in an unmodeled type context (e.g. a type-parameter default), and named-library VHDL references. This clears the last roadmap-coexistence gap and the "source-unit tokenizing" need for this use case.
0.12.0 design accepted: module/entity multi-version coexistence — June 2026¶
- [x] Design for extending name-mangling beyond packages to SV modules/interfaces/programs
and VHDL entities is accepted and written up in
docs/design/module-entity-coexistence.md. Approach
A (in-house scoped rewriter, zero new dependencies) — recognize declaration +
instantiation/reference positions for the known colliding names only, extending the
existing comment/string-aware token machinery rather than adding a real HDL parser. Key
decisions: generate-nested instantiations are in scope (both languages — common in real
designs; they reduce to the same flat-token instantiation shape), SV interfaces included
(with their extra type/
virtual/modport reference positions), and a classify-all-or-refuse safety model (rename only when every occurrence of a name is provably a declaration, instantiation/reference, or inert — otherwise refuse, never corrupt). Implementation lands as small staged commits (planner core → SV modules → VHDL entities → SV interfaces → integration → demo build-lane proof → docs), each gates-green, toward the0.12.0release.
Release-plan change: continuous 0.x iteration, no 1.0 soak — June 2026¶
- [x] Dropped the one-shot
1.0.0-rc.1soak / human-gated 1.0 sign-off in favor of continuous0.xiteration. Rationale: the heaviest remaining work — a real HDL parser + module/entity coexistence, and richer IP-XACT — can still move theip.toml/ip.lock/CLI shapes, so freezing the formats at1.0.0now would be premature. The project instead ships ordered0.xcapability releases through the Open Non-Blocking backlog (0.12.0parser + coexistence,0.13.0Git-registry hardening + richer IP-XACT, then supply-chain / confidential IP), keeping the SemVer0.xlicence to change formats. Validation moves from a single frozen candidate to continuous exercise via thehdlpkg-livetestproject and the consumer demo.1.0.0becomes a deliberate freeze for later, not a tracked near-term gate. Removed the soak/1.0-gate language from the Release plan, Current Status, Roadmap, Blocking Issues, the/releaseskill, the README, and the marketing deck; the superseded1.0.0-rc.1prerelease was yanked from PyPI and its tag/GitHub pre-release removed.
Release 0.11.0 — June 2026¶
- [x] Cut
0.11.0, an additive/internal release bundling the0.10.0-trial fixes and the work after it — noip.toml/ip.lock/CLI/registry-protocol break (so it continues the soak toward the human-gated1.0.0): User-Agent fix (#12),genfrom installed/cached/published deps (#13), the Git-backed registry (git+...), IP-XACT 1685-2014 XSD validation (+fileTypefix), the VHDL package-mangling GHDL fix, and the module/entity-coexistence decision. Bumpedpyproject.toml+src/hdl_ip_packager/__init__.pyto0.11.0. The release/code-reviewcaught and fixed two issues on the branch:gen --lockednow fails closed on checksum drift (it fetched from a registry without comparing the digest to the lock, unlikeinstall --locked), and the package mangler now refuses a mangled-name collision (two opaque versions differing only by a separator run could flatten to one HDL-safe name). Git-registry ref-resolution follow-ups were filed in Open Non-Blocking. Next:1.0.0sign-off.
Module/entity coexistence — decision to keep refusing + sharpened error — June 2026¶
- [x] Decided not to build module/entity multi-version coexistence; kept the refusal,
made it intentional and well-explained. Evaluated extending the name-mangler beyond
packages to SV modules/interfaces and VHDL entities. Conclusion: an SV module
instantiation (
foo u_foo (...)) cannot be disambiguated from other constructs by the token-based mangler — it genuinely needs a full HDL parser (the parked "source-unit tokenizing" item), and a heuristic rewrite could silently corrupt a design. A VHDL-entity subset using the unambiguousentity work.Xform is feasible parser-free but was consciously deferred (it would not cover SV). Sharpened the_reject_unmangleableBackendErrorinmangle.pyto say why (instantiation is ambiguous without a parser) and what to do (resolve to one version, or expose the shared logic as a package); added a test asserting the why-and-remedy guidance. The item stays in Open Non-Blocking with the full feasibility analysis; revisit when a parser frontend lands. No format/CLI/behavior change beyond the clearer message.
Consumer-demo toolchain build lane (Verilator + GHDL) + VHDL mangling fix — June 2026¶
- [x] The consumer demo now compiles and elaborates the designs
genemits with the real toolchains — not just asserts the flow files look right. A newbuild_hdl.pyharness in the consumer demo regenerates each flow and feeds it to the real toolchain — Verilator for the SystemVerilog SoCs (soc/,soc_conflict/), GHDL for the VHDL one (soc_vhdl/) — skipping a target whose tool is absent (so it is a no-op locally) and failing under--requirefor the dedicated, Ubuntu-onlybuildCI lane that installs the toolchains.verilator --binarycompiles + elaborates + links each SV design (the SoCs are$finish-less structural stubs, so the harness stops at the linked model rather than running it forever); the GHDL flowgenemits includes the run, and the event-less VHDL design returns at time zero.test_build_hdl.pycovers the harness's pure logic in the toolchain-free matrix. - [x] The build lane immediately earned its keep — it caught a real tool bug. The VHDL
package name-mangler emitted
vbus__v1_1_0(double underscore), which is valid SystemVerilog but invalid VHDL (consecutive underscores are illegal in an identifier), so the name-mangled coexistence designs compiled in SV but would not analyze in GHDL — a defect the text-onlygenassertions could never have surfaced. Fixedmangled_name(mangle.py) to use a single_vseparator and collapse any run of underscores, so the result is a legal identifier in both languages (bus_pkg/vbus->bus_pkg_v1_1_0/vbus_v1_1_0); a package name maps to one mangled name shared by all consumers, so a uniform, both-legal scheme is required. Added a regression test asserting the result never contains__and never starts/ends with_; updated the SV and VHDL planner/integration assertions. The demo also carried a fixture bug the lane found — a FIFO signal namedcell(a Verilog-2001 config reserved word) — renamed toslot. This lane feeds 1.0 third-party-consume confidence but does not itself gate the release.
IP-XACT export validated against the official 1685-2014 XSD — June 2026¶
- [x]
export-ipxactoutput is now proven schema-valid against the official Accellera IEEE 1685-2014 XSD, not just well-formed. The complete schema set (23 files) is vendored verbatim undertests/schema/ipxact-1685-2014/(a test fixture only — not in the wheel; see itsNOTICE.md— Accellera's license permits verbatim redistribution), andtests/unit/test_ipxact_xsd.pyvalidates the example cores + customfileTypecases withlxml(added to thedevextra), plus a negative test proving the validator rejects non-conformant XML. The example cores already validated — but the check caught a real bug: a filesettypeoutside the 1685-2014fileTypeenumeration (a custom type, orvhdlSource-2008, which the manifest accepts and the recent glob/generator-IP feature encourages) was emitted as a raw value, which is invalid IP-XACT. Fixedto_ipxactto map any non-enum type to the IP-XACT escape<fileType user="<type>">user</fileType>(new_FILE_TYPE_ENUM+_file_type). Additive/internal — noip.toml/ip.lock/CLI change. Files:ipxact.py,tests/unit/test_ipxact_xsd.py,tests/schema/…,pyproject.toml. (Richer mapping + IP-XACT 2022 remain a separate Open Non-Blocking item.)
Git-backed registry with commit provenance — June 2026¶
- [x] A Git repository of cores is now a registry, via a new
git+...://location (e.g.git+ssh://bitbucket.org/org/ip-registry.git, optional@<ref>for a branch/tag/ SHA).registry_from_locationdispatchesgit+schemes to a newGitRegistrythat clones/fetches the repo into a cache (~/.hdlpkg/git, overridable withHDLPKG_GIT_CACHE), checks out the requested ref (default: the remote's default branch), and mirrorsLocalDirectoryRegistryover the working tree — so discovery/manifest/ artifact reuse the proven local-scan path. Closes the traceability gap the trial raised:source_forreturnsgit+<url>@<commit-sha>, so the lockfile binds each pinned core to an immutable commit (on top of the existing SHA-256 content digest). Authentication is delegated to the user's own git config (ssh keys / credential helpers);GIT_TERMINAL_PROMPT=0keeps a missing credential from hanging. Purely additive — deps stay VLNV-based, noip.toml/ip.lockformat change (the lockfilesourceis already a free-form string). Files:registry.py(GitRegistry,_parse_git_locationwithgit@hostvs@refdisambiguation,default_git_cache_root),__init__.py, the_REGISTRY_HELPstring + regeneratedman/hdlpkg.1,tests/integration/test_git_registry_cli.py(a real local bare repo: resolve provenance, pull, ref checkout, unknown-ref error — skips where Controlled Folder Access blocks git in%TEMP%, runs in CI),tests/unit/test_registry_location.py(ref parsing). A per-dependency git source ({ git = …, rev = … }) remains intentionally out of scope — it would change the manifest format. Targeted for the upcoming0.11.0.
Trial fix: gen consumes installed/cached and published dependencies (#13) — June 2026¶
- [x]
hdlpkg gencan now build against dependencies that exist only as.ipkgblobs — in a published registry or the installed cache — not just loose--searchsource trees. Fixes the trial blocker whereinstallpopulated the cache butgencould not use it:_cmd_genbuilt each dependencyCoreSourcefromregistry.core_dir(vlnv), which onlyLocalDirectoryRegistryimplements. Addedgen --registry LOCATIONand--cache-dir DIR, and a_dependency_sourcehelper that picks, in order: (1) a locked artifact already in the cache by its lockfile digest — soinstall --lockedthengen --lockedworks fully offline; (2) a loose--searchtree, used in place (unchanged); else (3) fetch the.ipkgfrom the selected registry into the cache and extract it into a digest-keyed dir (<cache>/src/<digest>/, reused). Downstream (_materialize_filesets,_gen_core, the pureedamassembly) only readsource.root, so they work unchanged on extracted trees. A missing-and-unreachable locked dep fails with an actionable "runhdlpkg install … --locked" message. Files:cli.py(the helpers reuseextract_ipkg+ the content-addressed cache, mirroringpull),tests/integration/test_gen_registry_cli.py(registry-fetch, offline install→gen, and the error path), regeneratedman/hdlpkg.1. Additive — noip.toml/ip.lockformat change; closes the Open Non-Blocking "gen straight from a registry" item.
Trial fix: send a non-default User-Agent on every registry request (#12) — June 2026¶
- [x] Registry HTTP/OCI requests now carry
User-Agent: hdlpkg/<version>, fixing a trial blocker: the customer's JFrog Artifactory behind Cloudflare rejectedurllib's defaultPython-urllib/3.xUA (Browser Integrity Check → 403), breaking publish/pull/login whileoci+http://localhostworked. Added a module-level_USER_AGENTinjected at all four request sites —HttpRegistry._headers(_get/_put),OciRegistry._headers(_send), and the Basic-auth request in_exchange_token. The UA reads the package__version__; to make that importable fromregistry.py(which loads during__init__),__version__moved above the submodule imports in__init__.py, removing a latent circular-import hazard. Files:registry.py,__init__.py,tests/unit/test_registry_user_agent.py(fakes the transport; asserts every request — HTTP GET/PUT, OCI send, token-exchange — carries a non-default UA). Internal/robustness only — noip.toml/ip.lock/CLI/registry-protocol change; does not affect the soak.
Release 0.10.0 — June 2026¶
- [x] Cut
0.10.0, an additive docs release: ships the generatedhdlpkg(1)man page (see the milestone below) in the wheel as packaged data (share/man/man1/hdlpkg.1) and in the sdist, soman hdlpkgis available after a system/pipx install. Noip.toml/ip.lock/ CLI/registry-protocol change — it continues the0.9.0re-soak toward1.0.0rather than resetting it. Bumpedpyproject.toml+__init__.pyto0.10.0(and regeneratedman/hdlpkg.1, whose.THcarries the version).
hdlpkg(1) man page, generated from the CLI — June 2026¶
- [x] Added a full
man hdlpkgmanual, generated so it cannot drift from the CLI. A newscripts/gen_manpage.pyintrospectscli.build_parser()for the command + option reference and merges it with curated prose (description, the typical producer/consumer workflow, versioning/conflicts, registries, files, examples), emitting groffman(7)source toman/hdlpkg.1(LF-only; deterministic — no embedded date — so--checkis a reliable gate). The man page is shipped in the wheel as packaged data ([tool.hatch.build.targets.wheel.shared-data]→share/man/man1/hdlpkg.1, so a system/pipx install puts it on theMANPATH) and in the sdist;man/README.mddocuments viewing (man ./man/hdlpkg.1) and manual install. Verified to render warning-free throughgroff/man. Files:scripts/gen_manpage.py,man/hdlpkg.1,man/README.md,pyproject.toml,tests/unit/test_manpage.py(covers every subcommand + a committed-page up-to-date gate);README.md,docs/INDEX.md,docs/user_guide.md. Docs-only / additive — noip.toml/ip.lock/CLI/protocol change, so it does not affect the0.9.0soak.
Release 0.9.0 — June 2026¶
- [x] Cut
0.9.0, the stable pre-1.0 release that resets the1.0.0-rc.1soak. The rc.1 candidate aimed to freeze the on-disk formats, but the third-party trial proved the[filesets]format incomplete (explicit-file-only lists are unworkable for a large generated/vendored IP), so rather than freeze a format we already know must change, the candidate is re-cut as0.9.0— formats explicitly still open — folding in the two trial findings: glob/directory filesets andhdlpkg init --schemefor non-SemVer version codes (both detailed in the entries below).0.9.0is published to PyPI as a normal (non-pre) release, so it becomes the latest stable version (plainpip install hdl-ip-packagerresolves to it); the earlier1.0.0-rc.1pre-release is left behind as superseded. Bumpedpyproject.toml+__init__.pyto0.9.0. Next: re-soak toward1.0.0— a clean third-party publish/consume against0.9.0with no further format change is the freeze signal; promotion to1.0.0stays a human-gated stability sign-off.
Soak finding: glob + directory filesets (resets the rc.1 soak; next cut is 0.9.0) — June 2026¶
- [x]
[filesets]filesentries now accept globs and directories, closing a format gap the1.0.0-rc.1third-party trial surfaced: a customer packaging a large, script-generated / vendored IP (a Vivado generator script + an IP-XACT submodule tree + HDL) found that an explicit per-file list was unworkable ("just too big"). Afilesentry may now be a literal path (as before), a glob (any entry with*/?/[;**recurses, e.g.rtl/**/*.vhd), or a directory (packs every file under it, recursively). Expansion happens at the I/O boundary atpack/publish/gentime, with matches sorted so the.ipkgstays byte-identical (deterministic content address preserved); an entry matching no file is an error, and../absolute patterns are rejected. The manifest keeps the patterns as authored, soip.lockand the SBOM are unchanged. Existing literal-list manifests are unaffected (backward-compatible). - [x] Implementation: a new pure-at-the-boundary
expand_fileset_files(core_dir, fileset_name, patterns)inpackaging.pydrives bothpack(_collect) andgen(the CLI materializes eachCoreSource's filesets viadataclasses.replacebefore assembly, so the pureedambackend keeps seeing a concrete file list and never emits a raw glob). Files:packaging.py,cli.py,tests/integration/test_packaging.py,test_gen_cli.py;docs/user_guide.md,docs/modules/manifest.md. Release impact: this is anip.tomlsemantic change, so under the soak rule it resets the1.0.0-rc.1soak — the next candidate is0.9.0(folding in globs and the additiveinit --schemefinding), re-soaking toward1.0.0. The format-vs-soak call was made deliberately (fix the fileset format before the freeze); the0.9.0cut itself is the human-gated release step.
Soak finding: hdlpkg init --scheme for non-SemVer version codes — June 2026¶
- [x]
hdlpkg initnow exposes the version scheme, closing a trial-feedback gap: a third-party tester ranhdlpkg initwith a vendor version code (D5020204) and hiterror: Not a valid semantic version: 'D5020204', with nothing pointing at the already-supported non-SemVer schemes.inithard-codedVersion.parseand rendered a scheme-less manifest, so the one front door could not author theopaque/monotonic/calvercores the rest of the tool handles. Added aninit --scheme {semver,calver, monotonic,opaque}flag (threaded throughScaffoldOptions.create->parse_version(version, scheme)), which emits[package].schemein the scaffold when non-default, and made the SemVer parse error point at[package].scheme/init --scheme. Files:scaffold.py,cli.py,version.py,tests/unit/test_scaffold.py,test_cli.py;docs/user_guide.md. Purely additive (a new optional flag + a friendlier message + emitting an already-frozen manifest key): no change toip.toml/ip.lockshape, lockfile, or registry protocol, and existinginitinvocations are unchanged. This change is additive and would not by itself reset the soak; it now simply rides into the0.9.0re-cut alongside the glob/directory fileset change (which does reset it — see the entry above).
Release 1.0.0-rc.1 — June 2026¶
- [x] Cut
1.0.0-rc.1, the first 1.0 release candidate, to start the soak toward the final1.0.0. It bundles everything since0.8.0: the versioning contract for the 1.0 freeze (Cargo-style unification, the[resolution] on-conflictpolicy, SV+VHDL package name-mangling, ordered non-SemVer schemes), and the operational distribution protocol (local/HTTP/OCI registries behindregistry_from_location,hdlpkg loginwith direct bearer and OCI token-exchange +docker loginreuse). Published to PyPI as a pre-release (PEP 4401.0.0rc1), sopip install hdl-ip-packagerdoes not pick it up unless--pre/an explicit pin is used;release.ymlmarks the GitHub release pre-release. Bumpedpyproject.toml+__init__.pyto1.0.0-rc.1. The soak rule: this candidate must hold with noip.toml/ip.lock/CLI/registry-protocol change; a clean soak (ideally including a genuine third-party publish/consume against this rc) is promoted to1.0.0, while any required format change resets the soak and ships as0.9.0instead. Promotion to1.0.0remains a deliberate, human-gated sign-off (not autonomous).
OCI token-exchange auth flow (works against managed Harbor/cloud registries) — June 2026¶
- [x]
OciRegistrynow performs the Docker/OCI token-exchange dance, so it authenticates against registries that issue short-lived access tokens (managed Harbor, GitLab, Docker Hub, ECR/ACR), not only ones that accept a static bearer. On a401carryingWWW-Authenticate: Bearer realm=...,service=...,scope=..., the backend calls the realm token endpoint (HTTP Basic with the stored credential, or anonymously for a public pull token), caches the returned access token, and retries the request once. Because the retry happens per request using the server-supplied scope, a pull-scoped token is transparently upgraded to a push scope when publishing. A username-less credential is still sent directly as a bearer (the self-hosted/no-auth path that already worked is unchanged). New pure helperparse_bearer_challenge(unit-tested). - [x] Credentials grew a username.
Credential(secret, username=None)replaces the bare token: a username-less credential is a direct bearer; a username+secret pair is used as HTTP Basic in the exchange.hdlpkg logingained--username(and--passwordas an alias of--token), prompting for a password instead of a token when a username is given. The credentials file moved to a richer[registries."host"]form (with an optionalusername), and still reads the legacy[tokens]table so older files keep working. - [x]
docker logincredentials are reused.~/.docker/config.json($DOCKER_CONFIGhonored) is parsed forauths[host].auth(base64user:pass) andidentitytoken, and merged as a fallback under storedhdlpkg logincredentials, so a registry the user already authenticated with Docker works without a second login. - [x] Tested honestly with no live service: a localhost mock that requires the
exchange (401 challenge -> a Basic-checking token endpoint -> bearer-gated
/v2/) drives the full publish/resolve flow via the CLI, plus wrong-password failure and an anonymous pull-only token (resolve works, push is refused); the challenge parser,Credential/docker-config parsing, and the store round-trip have unit tests. Files:credentials.py,registry.py,cli.py,__init__.py,tests/unit/test_credentials.py,test_registry_location.py,test_login_cli.py,tests/integration/test_oci_auth_cli.py. Validated earlier against live no-auth Zot anddocker run registry:2; this closes the authenticated-registry gap noted then.
Stable registry protocol: HTTP + OCI backends behind one abstraction, with login auth — June 2026¶
- [x] Network registries are now first-class, so teams can share IP privately on their
own servers — the operational half of the 1.0 "stable registry/OCI protocol" gate.
resolve/install/tree/publish/pullaccept a--registrylocation dispatched by URL scheme through a singleregistry_from_location()factory: a bare path /path:/file://-> the writable localLocalRegistry,http(s)://->HttpRegistry, andoci:///oci+http://-> the newOciRegistry. The CLI is now backend-agnostic (the one place a backend is chosen is the factory), which is what makes the on-disk and wire protocol surface stable for 1.0. - [x]
OciRegistry: cores as OCI artifacts over the OCI distribution v2 API, so a core lives in any standard registry (Harbor, Artifactory, Nexus, GitLab, Zot, ECR/ACR) — all of which are self-hostable and private by default. A core'sip.tomlis the artifact config blob and its deterministic.ipkgis the single layer, tagged with the version; the package maps to repository{prefix}/{vendor}/{library}/{name}. Implements blob upload (HEAD-skip + POST/PUT monolithic), manifest/tag PUT+GET, andtags/list, append-only (refuses to overwrite a tag).oci://uses HTTPS,oci+http://plaintext (internal/dev). Because the layer is the.ipkg, its OCI digest is the content address the cache keys on and the lockfile pins. - [x]
HttpRegistrypromoted to a writable, authenticated network registry (was a read-only static index): reads viaGET, publishes viaPUT(so anyPUT-capable store — a small service, object storage, WebDAV — can host it), append-only, opaque-version tolerant. - [x]
hdlpkg login/logout+ a credentials subsystem (credentials.py) make the network backends private. A pureCredentialStoremaps a registry host to a bearer token (hosts share one token across repos) with TOML serialization; the thinload_credentials/save_credentialspair is the only I/O, writing~/.hdlpkg/credentials.toml(override withHDLPKG_CREDENTIALS) owner-only where the OS allows. Every network request carriesAuthorization: Bearer <token>, so resolve/install/ publish against a private registry "just work" after one login; missing/wrong credentials fail closed. (The Docker token-exchange flow for managed registries is a tracked refinement; the stored token is presented directly today.) - [x] Tested honestly with no live service: a localhost auth+
PUTHTTP server and a minimal in-memory OCI distribution v2 mock exercise the full publish -> resolve -> install -> pull flow through the real CLI for both backends, plus append-only, auth-required, and error paths; theCredentialStore,registry_from_locationdispatch, andlogin/logouthave unit tests. Files:src/hdl_ip_packager/credentials.py,registry.py,cli.py,exceptions.py(CredentialsError),__init__.py,tests/unit/test_credentials.py,test_registry_location.py,test_login_cli.py,tests/integration/test_http_registry_cli.py,test_oci_registry_cli.py. Remaining toward 1.0: a third-party publish/consume and a1.0.0-rc.1soak (see the Release plan); Git backend, the OCI token-exchange flow, andgen-from-registry stay Open Non-Blocking.
VHDL package name-mangling for multi-version coexistence — June 2026¶
- [x]
gennow name-mangles coexisting VHDL packages too, the direct analogue of the SystemVerilog-package work, so two versions of a shared VHDL package build together under[resolution] on-conflict = "isolate_namespaces"(e.g. via theghdltoolflow). A new VHDL-aware lexer (mangle.py) — case-insensitive,--//* */comment- and string-aware — rewrites a package name only in the unambiguous VHDL positions:package <name>/package body <name>declarations,end [package [body]] <name>labels, anduse work.<name>...references. Each consumer'suseclause is routed to the version it resolved to (vfifo->vbus__v1_1_0,vlegacy->vbus__v2_0_0). VHDL's reference contexts are structured (use/work.), so this is as safe as the SV-package case — no full parser. - [x] The mangler became language-aware.
GenSourceFilenow carries alanguage(from the fileset type) instead of an SV-only flag;plan_package_manglingdispatches declared-name scanning and rewriting per language, and refuses what it cannot do safely: a colliding module/interface (SV) or entity (VHDL) — instantiation position is ambiguous without a real parser — or an unknown source language. Named- libraryuseclauses (anything other thanwork.) are left untouched (a documented limitation; everything is analyzed intowork). New:declared_vhdl_packages,declared_vhdl_entities,rewrite_vhdl_packages. Verified end to end against the consumer demo's new VHDLsoc_vhdl(avbuspackage at two majors,ghdlflow). Files:mangle.py,cli.py,backends/edam.py,resolver.py(warning text),__init__.py,tests/unit/test_mangle.py,tests/integration/test_mangle_vhdl_gen_cli.py.
Physical multi-version coexistence at gen: SystemVerilog package name-mangling — June 2026¶
- [x]
gennow builds two versions of one SystemVerilog package together instead of refusing — the physical half of multi-version coexistence. Under[resolution] on-conflict = "isolate_namespaces"(the policy that keeps incompatible versions),genautomatically name-mangles each package version to a unique name (bus_pkg->bus_pkg__v1_1_0/bus_pkg__v2_0_0) and rewrites every consumer's references to the version it resolved to (fifo->__v1_1_0,legacy->__v2_0_0), so both elaborate in HDL's one global namespace. The rewritten sources are materialized into<output>/src/<vlnv>/…and the generated tool file points at those copies; the originals on disk are untouched. Verified end to end against the external consumer demo'ssoc_conflict/. - [x] Safe by construction, no full parser. A new pure
mangle.pycarries a comment/string-aware SystemVerilog scanner that rewrites a package name only in the syntactically unambiguous positions —package <name>/endpackage : <name>declarations,import <name>::, and<name>::scoped references — so a coincidental signal namedbus_pkg, or the name inside a comment or string, is never touched. The pureplan_package_manglingcomputes per-file rename maps (a package's own declaration vs a consumer's resolved version) and refuses what it cannot do safely: two versions of a module/interface (instantiation position is ambiguous without parsing) or any non-SystemVerilog (VHDL) source — those still get a clearBackendError. Documented limitation: a macro that constructs a package name by token pasting is left untouched. Files:mangle.py,backends/edam.py(build_eda_design(allow_multiversion=…)+ multi-version-safe topo sort),cli.py(genmaterializes the mangled tree, warns, reports),__init__.py,tests/unit/test_mangle.py,tests/integration/test_mangle_gen_cli.py. (Remaining: module/interface coexistence and VHDL — both deferred as needing a real HDL frontend.)
Ordered non-SemVer schemes: CalVer + monotonic — June 2026¶
- [x] Added two ordered non-SemVer version schemes behind
[package].schemeso such cores can be ranged and newest-selected, not just exact-pinned.scheme = "calver"carries ordered numeric date/calendar versions (2024.1,2024.10,2025.2.3) with the first component (year) as the compatibility boundary:^2024.1==>=2024.1, <2025,~2024.1==>=2024.1, <2024.2, and same-year dependents unify (year-as-major, Cargo-style).scheme = "monotonic"carries a single ordered revision (r3,rev12,12); all revisions are one compatibility group (newer supersedes), so^r3==>=r3selects the newest while~r3/=r3pin exactly. Two distinct exact monotonic pins are a hard unsatisfiable failure (one shared group), not a coexistence. - [x] Implementation: new ordered
CalVerandMonotonicVersionvalue types and aparse_version(text, scheme)dispatcher inversion.py;VersionConstraintgained a deferred ordered-clause path (ordered) that interprets^/~/ranges against the candidate's scheme at match time (the dependency's scheme is unknown at parse) — for non-SemVer schemes a bare constraint means exact (those schemes lack SemVer's caret default; use^/~/ranges explicitly).compatibility_group, the resolver's_edge_nodegrouping, manifest/Vlnvparsing, and the lockfileschememarker all extend to the new schemes;LocalRegistry.versionsalready recovers a non-SemVer version directory from its manifest. Files:version.py,manifest.py,vlnv.py,resolver.py,lockfile.py,__init__.py,tests/unit/test_version.py,test_resolver.py,test_manifest.py,test_vlnv.py,test_lockfile.py. (Remaining versioning work is now only the physical multi-version coexistence atgen— name-mangling.)
Versioning contract for the 1.0 freeze: conflict policies, multi-version, opaque scheme — June 2026¶
- [x] Settled the resolver contract that was gating the 1.0 format freeze —
multi-version coexistence (bookkeeping), unification semantics, and the non-SemVer
scheme floor, the three Open Non-Blocking versioning issues. The resolver now
always unifies SemVer-compatible dependents (same major -> newest satisfying,
Cargo-style; a diamond on
^1.0+^1.1still collapses to one1.1.x) and gives the user a conflict policy for a genuinely incompatible conflict (two majors, or two distinct exact pins of anopaquecore), set by a rootip.toml[resolution] on-conflictkey with a--on-conflictCLI override: fail_on_conflict(default) — raiseResolutionError(preserves prior behaviour; the demo'ssoc_conflict/still fails by default).use_latest— collapse to the newest of the conflicting versions, prune orphans, and warn that lower requirements may be violated.isolate_namespaces— keep every incompatible version in the resolve/lock/tree (multi-version bookkeeping, part (a) of the coexistence issue). Physical coexistence (name-mangling, part (b)) is not built, sogenrefuses to emit two versions of one package with a clear message (backends/edam.py).- [x] Opt-in
[package].schemeversion scheme (semverdefault, oropaque) and explicit non-SemVer rejection. A non-SemVerpackage.versionis rejected under the default semver scheme at parse time with a clearManifestErrornaming the string (the gating minimum (1) of the non-SemVer issue).scheme = "opaque"treats versions as opaque tokens: dependents must pin an exact=version and every distinct pin is its own compatibility group (honor-exact-pins), so the resolver never assumes compatibility it cannot verify. - [x] Genuinely non-SemVer version strings under
opaque— a newOpaqueVersiontoken (e.g. a vendor part numberD5020100, calver2024.1,r3) is threaded throughVlnv(a scheme-awareVlnv.parse), the manifest,VersionConstraint(exact-pin opaque constraints like=D5020100, with^/ranges refused), the lockfile (round-trips via ascheme = "opaque"marker per package), the tree, and the registry (LocalRegistry.versionsreads the manifest for a non-SemVer version directory) -- andpull/yank, which take a VLNV string with no scheme, parse it as SemVer first and fall back to an opaque token (cli._user_vlnv), so pulling an opaque core by VLNV works. Verified against the consumer demo's newsoc_opaque/(vendor IP atD502../D401../DB..part numbers, resolved by exact pin,gen-built, published + pulled). (The ordered non-SemVer schemes — calver/monotonic — landed next; see the milestone above.) - [x] Implementation: a pure
compatibility_group(version, scheme)andVersionConstraint.is_exact/exact_versioninversion.py; a grouped, scheme-aware backtracking solver keyed per(package, compatibility-group)node with a post-search policy fold and a reachability pass that prunesuse_latestorphans (resolver.py);Resolutionnow exposesvlnvs/by_ref/warningsand may carry more than one version per package;treeview.pypicks the per-edge version and expands per VLNV; the CLI threads the policy through resolve/install/tree/gen and prints warnings to stderr. Verified end to end against the external consumer demo'ssoc_conflict/(default fails;isolateresolves twobus_pkg;genrefuses;use_latestcollapses to2.0.0),soc/(diamond still unifies to onebus_pkg 1.1.0), andsoc_opaque/(opaque vendor part numbers). Files:version.py,manifest.py,resolver.py,treeview.py,vlnv.py,lockfile.py,registry.py,backends/edam.py,cli.py,__init__.py,tests/unit/test_version.py,test_resolver.py,test_treeview.py,test_manifest.py,test_vlnv.py,test_lockfile.py,test_edam.py,tests/integration/test_conflict_policy_cli.py.
Hard gate: all PR checks must be green before merge — June 2026¶
- [x] Green CI is now an explicit, hard gate before any merge. A real incident
drove this: PR #8 was merged while the
Test (py3.12, windows-latest)check was red — a transientactions/setup-pythonflake (theInstall/Teststeps were skipped, not failed;main's own push CI was green, so the code was fine). Two process bugs allowed it: the merge step watched a single workflow run instead of all PR checks, and pipedgh run watchtotail, which hid the non-zero exit code sogh pr merge --adminran anyway (and--adminbypasses required checks). Fix: the merge gate is nowgh pr checks <branch> --watch(exit 0 over the whole matrix, never piped to a pager),--adminis documented as covering only the self-approval requirement (never a red/pending check), and a flaky run is re-run to green (gh run rerun <id> --failed) rather than bypassed. Updated.claude/commands/release.md,CLAUDE.md,docs/ai_agent_instructions.md.
Branch model + agent-driven release flow + GitHub Release on tag — June 2026¶
- [x] Adopted a
develop(working) +main(release line) branch model; PRs are release-only. Day-to-day work now commits directly todevelopwith no PR (/tackle-issuestep 7 just makes the gates green and commits).mainis the protected release line, updated only through the release flow: arelease/X.Y.ZPR cut offdevelop, which — once CI is green — the agent reviews with/code-review(fixing in-scope findings, filing out-of-scope ones in Open Non-Blocking Issues) and merges with a merge commit (gh pr merge --merge --admin; GitHub forbids self-approval, so--adminsatisfies the ruleset and logs the bypass), then tagsmainand fast-forwardsdevelop. A human gate applies only when the agent cannot safely decide on its own — the1.0.0sign-off, a security-sensitive or hard-to-reverse change, or anything the user reserved. Mirrors the reference project'sdevelop/mastersplit. UpdatedCLAUDE.md,docs/ai_agent_instructions.md,README.md,.claude/commands/release.md, and.claude/commands/tackle-issue.md. - [x]
release.ymlcreates a GitHub Release for each tag. A newgithub-releasejob (gated onneeds: publish, so it only announces what reached PyPI) builds the body from the tag'sdocs/progress_tracker.mdentry plus a link to the PyPI page and attaches the wheel + sdist. The body logic is a pure, unit-tested helper (scripts/extract_release_notes.py:extract_section/build_release_body, falling back to a one-line summary when the tracker has no entry, e.g. a pre-release). Pre-release tags (X.Y.Z-rc.N) are marked--prerelease. Files:.github/workflows/release.yml,scripts/extract_release_notes.py,tests/unit/test_extract_release_notes.py,docs/INDEX.md.
Release 0.8.0 — June 2026¶
- [x] Tagged
0.8.0per the Release plan: ships the pre-1.0 completeness pass + the non-blocking/backlog batch that landed onmainafter the0.7.0tag — reproducible lockfile-driven builds (install --locked/gen --locked),hdlpkg add, the optionalip.tomlschemakey, pack-path + tool-flowtophardening, thehdlpkg treeWindows (cp1252) fix,resolve/install/tree --registryconsuming a publishedLocalRegistry,Fileset.depend-aware EDAM assembly, three more tool-flow backends (Icarus/GHDL/Yosys), Dependabot, and the per-module user manual. Shipped as0.8.0rather than1.0.0: theip.toml/ip.lockformats are still moving (multi-version coexistence, unification semantics, and non-SemVer version schemes are now recorded as Open Non-Blocking Issues) and the 1.0.0 stability gate (anrcsoak, the OCI registry protocol, a third-party publish/consume) is not yet met. Bumpedpyproject.toml+__init__.py. Also recorded the three versioning issues above.
Pre-1.0 completeness pass — June 2026¶
- [x] Reproducible, lockfile-driven builds (
install --locked,gen --locked). Both build exactly from a committedip.lockwithout re-resolving (thenpm ci/cargo --lockedmodel), verifying fetched digests against the lock and failing if it is missing;hdlpkg resolveremains the one command that updates the lock. Closes the "the lockfile isn't actually consumed" gap. Files:cli.py,tests/integration/test_locked_cli.py. - [x]
hdlpkg addinserts/updates a dependency inip.tomlvia a pure, text-preserving line editor (editing.py) — keeps formatting/comments, refuses a self-dependency, and re-validates before writing. The last planned CLI stub is gone (the empty planned-command machinery was removed). Files:editing.py,cli.py,tests/unit/test_editing.py,tests/unit/test_cli.py. - [x]
ip.tomlschema version — an optional top-levelschemakey (default 1); a manifest written for a newer schema is rejected with a clear message rather than mis-parsed, giving the format a migration path before the 1.0 freeze. Files:manifest.py,tests/unit/test_manifest.py. - [x] Hardening —
packnow rejects fileset paths that escape the core directory (../absolute); the tool-flowtopis validated as a safe HDL identifier before it is interpolated into generated scripts; and the per-backend "missing top" / "unsupported file type" guards were factored onto theBackendABC (resolving the two PR-review findings). Files:packaging.py,backends/base.py,backends/*.py,tests/integration/test_packaging.py,tests/unit/test_backends.py. - [x]
hdlpkg treeWindows fix — switched the dependency-tree connectors from Unicode box-drawing characters to ASCII; the Unicode form raisedUnicodeEncodeErroron a default cp1252 Windows console. Surfaced by an external consumer demo project (a diamond-dependency SoC built against the tool in a venv). Added an ASCII-output regression test. Files:treeview.py,tests/unit/test_treeview.py. - [x] Resolve/install/tree from a published registry (
--registry DIR). These commands previously only scanned local source trees (--search); they can now resolve and fetch directly from aLocalRegistrythathdlpkg publishwrote, closing the producer->consumer loop for local registries (the gap the demo surfaced). AddedRegistry.source_for(the lockfile recordsregistry:<dir>). HTTP/OCI registries andgen-from-registry remain open. Files:cli.py,registry.py,tests/integration/test_registry_resolve_cli.py.
Non-blocking + backlog batch (develop) — June 2026¶
- [x] Richer dependency fileset selection — honor
Fileset.depend.backends/edam.pynow expands each selected fileset's declareddependclosure (transitively, deps emitted before the fileset, de-duplicated, cycle-safe), for both the root target's filesets and a dependency's exported surface. A core can thus state exactly what a fileset needs (e.g. anrtlthat depends on apkgfileset) instead of relying on thertl/tbnaming convention alone. Files:src/hdl_ip_packager/backends/edam.py,tests/unit/test_edam.py. - [x] More tool-flow backends — Icarus Verilog, GHDL, Yosys. Added three pure
Backendimplementations behind the existinggeninterface:IcarusBackend(a.cmdsource list +run_iverilog.sh),GhdlBackend(run_ghdl.shanalyze/ elaborate/run, VHDL-only), andYosysBackend(a.yssynth script). Each rejects file types it can't handle and a missing top.gennow supports five tool flows (verilator,vivado,icarus,ghdl,yosys). Files:src/hdl_ip_packager/backends/{icarus,ghdl,yosys}.py,backends/__init__.py,tests/unit/test_backends.py. - [x] Dependabot configuration (backlog). Added
.github/dependabot.ymlwatching thepip(pyproject tooling) andgithub-actionsecosystems with weekly, grouped, low-noise PRs — which also surfaces action-runtime deprecations (e.g. the Node 20 bump) automatically. File:.github/dependabot.yml.
Still open (need a live external service to build/test honestly): Git-backed and
OCI registry backends, Sigstore (cosign) signing. Still parked (judgment): the
uv/hatch build-backend switch (churn; hatchling works), source-unit
tokenizing (large), mutmut (slow, unused without a run), and IP-XACT XSD
validation (needs the bundled Accellera XSD). These land via develop and ship in
the next release.
Release 0.7.0 — June 2026¶
- [x] Tagged
0.7.0per the Release plan: supply-chain (M8) —hdlpkg pack --sbomemits a deterministic CycloneDX 1.5 SBOM alongside the.ipkg, on top of the SHA-256 content addressing that already pins integrity. Shipped as0.7.0rather than1.0.0because theip.toml/ip.lock/CLI formats are still pre-1.0 and the 1.0.0 stability gate (third-party publish/consume, anrcsoak, frozen formats) is not yet met; Sigstore signing remains an open issue. With M1–M8 all delivered, the next release boundary is1.0.0(a deliberate stability sign-off, not a feature milestone). Bumpedpyproject.toml+__init__.py.
M8 — Supply-chain: CycloneDX SBOM at pack time — June 2026¶
- [x] Implemented deterministic SBOM generation (
sbom.py) and wiredhdlpkg pack --sbom. A purebuild_cyclonedx(root, dependencies)renders a CycloneDX 1.5 JSON SBOM: the packed core as themetadata.component, its resolved dependency manifests ascomponents(each with a VLNVbom-ref,group,purlpkg:generic/..., and licence), and thedependenciesgraph of edges. Output is deterministic by construction (sorted keys, components/edges sorted by VLNV, no timestamp or random serial number) so the same inputs pack to byte-identical SBOM bytes. The CLIpack --sbom [FILE]is the thin wrapper: it writes the SBOM alongside the.ipkg(default<vlnv>.cdx.json), resolving the dependency graph over--searchso the SBOM pins concrete versions. Together with the SHA-256 content addressing that already pins every artifact across the cache, lockfile, and registry, this delivers the integrity + bill-of-materials half of the supply-chain milestone. Deferred (new Open Non-Blocking Issue): Sigstore (cosign) keyless signing of the artifact + SBOM, which needs OIDC/Fulcio/Rekor infrastructure (or a managed key + transparency log) to build and test honestly — the same external-service constraint that deferred the Git/OCI backends. Exposedbuild_cyclonedx/CYCLONEDX_SPEC_VERSION. Files:src/hdl_ip_packager/sbom.py,src/hdl_ip_packager/cli.py,src/hdl_ip_packager/__init__.py,.gitignore,tests/unit/test_sbom.py,tests/integration/test_sbom_cli.py.
Release 0.6.0 — June 2026¶
- [x] Tagged
0.6.0per the Release plan: IP-XACT (IEEE 1685-2014) export (M7) —hdlpkg export-ipxactwrites a component XML (VLNV + model views + fileSets) for tool interop. Bumpedpyproject.toml+__init__.py.
M7 — IP-XACT (IEEE 1685) export — June 2026¶
- [x] Implemented IP-XACT export (
ipxact.py) and wiredhdlpkg export-ipxact. A pureto_ipxact(manifest)renders an IEEE 1685-2014 component XML from a manifest: the VLNV identity, amodelwith oneview+componentInstantiationper[targets.*](carrying the target'smoduleNameandfileSetRefs), and thefileSetswith each file'sfileType. The manifest filesettypevocabulary (systemVerilogSource/verilogSource/vhdlSource) already is the IP-XACTfileTypevocabulary, so it passes through unchanged. Built with stdlibxml.etree.ElementTree(namespaced,ET.indent-formatted) for deterministic, dependency-free output; the CLIexport-ipxactcommand (removed from the planned stubs) is the thin wrapper that writes the XML (default<vendor>.<library>.<name>.<version>.xml). Output targets the 1685-2014 schema and is well-formed and structurally conventional; validating against the official Accellera XSD is deferred (new Open Non-Blocking Issue). Exposedto_ipxact/IPXACT_NAMESPACE. Files:src/hdl_ip_packager/ipxact.py,src/hdl_ip_packager/cli.py,src/hdl_ip_packager/__init__.py,.gitignore,tests/unit/test_ipxact.py,tests/integration/test_ipxact_cli.py,tests/unit/test_cli.py.
Release 0.5.0 — June 2026¶
- [x] Tagged
0.5.0per the Release plan: tool-flow generation (M6) —hdlpkg gen <target>turns a resolved design into Verilator (.vc) or Vivado (.tcl) inputs via a pure EDAM-like intermediate, andhdlpkg treeprints the resolved dependency graph. Bumpedpyproject.toml+__init__.py.
hdlpkg tree dependency view — June 2026¶
- [x] Added
hdlpkg treeto print the resolved dependency graph. A puretreeview.py(render_dependency_tree) takes the root manifest, the resolver's one-VLNV-per-package selection, and the resolved manifests, and renders an ASCII tree annotating each edge with its constraint and the chosen version (acme:x:mid ^1.0.0 -> 1.0.0). A package reached twice (diamonds) is expanded only on first occurrence and later marked(*)so the output is finite; unresolved edges are labelled(unresolved). The CLItreecommand (removed from the planned list) is the thin wrapper: it resolves over--searchagainst aLocalDirectoryRegistryand prints the tree. Exposedrender_dependency_tree. Files:src/hdl_ip_packager/treeview.py,src/hdl_ip_packager/cli.py,src/hdl_ip_packager/__init__.py,tests/unit/test_treeview.py,tests/integration/test_tree_cli.py.
M6 — Tool-flow generation (Verilator + Vivado) — June 2026¶
- [x] Implemented tool-flow generation in a new
backends/package and wired the realhdlpkg gen. A pure, tool-agnostic EDAM-like intermediate (backends/edam.py:EdaFile/EdaDesign+build_eda_design) turns the root core plus its resolved dependencies and a chosen[targets.*]into a flat, ordered source list with a top unit and a tool flow. Selection semantics: the root contributes its target's filesets (so asimtarget keeps its testbench, asynthtarget does not); a dependency contributes only its synthesizable surface (itsrtlfileset, or all non-testbench filesets by name) so a dependency's testbench never leaks into a dependent. Cores are emitted dependencies-first via a topological sort (ties by VLNV), file types are normalized to the IP-XACT vocabulary, and duplicate paths are de-duplicated. TwoBackendimplementations consume the intermediate (backends/base.pyinterface get_backend/supported_toolflowsregistry keyed ontoolflow):VerilatorBackendemits a<name>.vccommand file (--top-module+ sources; rejects VHDL and a missing top),VivadoBackendemits a<name>.tclsource script (read_verilog -sv/read_verilog/read_vhdl,set_property top,update_compile_order). The backends are pure (generatereturns{filename: text}); the CLIgencommand is the thin I/O wrapper that resolves dependencies over--search, assembles the design, renders, and writes the files into--output(defaultgen/<target>/). AddedBackendError; exposed the backend API from the package. Verified end to end withhdlpkg gen sim/synthover the bundledexamples/uart(the FIFO dependency's rtl is pulled in, its tb is not). Deferred (now Open Non-Blocking Issues): richer dependency fileset selection (honorFileset.dependinstead of the name heuristic) and more backends (Icarus/GHDL/Quartus/Yosys). Files:src/hdl_ip_packager/backends/(__init__.py,edam.py,base.py,verilator.py,vivado.py),src/hdl_ip_packager/cli.py,src/hdl_ip_packager/registry.py(addedcore_dir),src/hdl_ip_packager/exceptions.py,src/hdl_ip_packager/__init__.py,tests/unit/test_edam.py,tests/unit/test_backends.py,tests/integration/test_gen_cli.py,tests/unit/test_cli.py.
Release 0.4.0 — June 2026¶
- [x] Tagged
0.4.0per the Release plan: the full producer/consumer loop — a deterministic.ipkg, append-onlypublish(withyank), andpullby VLNV — works against a local registry, with the cache/lockfile pinning the packed-content digest. Bumpedpyproject.toml+__init__.py.
M5 — pack / publish / pull (+ .ipkg, yank) — June 2026¶
- [x] Implemented packaging and the distribution commands. New
packaging.pybuilds a deterministic.ipkg(gzip+tar ofip.toml+ every fileset file, with sorted entries, fixed mode/owner, and zeroed mtime/gzip header) so a core always packs to byte-identical bytes and its SHA-256 is a stable content address;extract_ipkgunpacks with path-traversal protection, andmanifest_from_ipkgreads the manifest back. The.ipkgis now the unified artifact across the stack:Registry.artifact_bytesreturns it (the local backend packs on read, the HTTP backend servescore.ipkg), so the cache key and lockfile checksum are the packed-content digest (replacing the M2 manifest-bytes stopgap). A new writableLocalRegistrystores cores under<root>/<vendor>/<library>/<name>/<version>/withip.toml+core.ipkg; publishing is append-only (re-publish refused) andyankdrops a.yankedmarker that hides a version from new resolves without breaking existing lockfiles. CLI:pack,publish,pull(fetch by VLNV into the cache, optionally extract), andyank(now real, removed from the planned stubs). AddedPackagingError; exposed the packaging +LocalRegistryAPI. Verified the full pack -> publish -> pull loop end to end with matching digests. Files:src/hdl_ip_packager/packaging.py,src/hdl_ip_packager/registry.py,src/hdl_ip_packager/cli.py,src/hdl_ip_packager/exceptions.py,src/hdl_ip_packager/__init__.py,tests/integration/test_packaging.py,tests/integration/test_pack_cli.py,tests/integration/test_registry.py,tests/integration/test_resolve_cli.py,tests/unit/test_cli.py.
Release 0.3.0 — June 2026¶
- [x] Tagged
0.3.0per the Release plan: cores can now be fetched from a registry into a verified, content-addressed cache (M3) via local-directory and HTTP backends (M4), withhdlpkg installresolving and installing in one step. Bumpedpyproject.toml+__init__.py. (Git/OCI registry backends remain Open Non-Blocking Issues.)
M4 — Registry backends (local + HTTP) + hdlpkg install — June 2026¶
- [x] Implemented the registry layer and wired
hdlpkg install.registry.pynow defines theRegistryABC (versions/manifest/artifact_bytes+ a sharedfetchthat stores a core's artifact in the content-addressed cache, and a defaultpublishthat errors until M5) plus two concrete backends:LocalDirectoryRegistry(discovers cores by scanning directory trees forip.toml) andHttpRegistry(a static HTTP index:{base}/{vendor}/{library}/{name}/versions.json+.../{version}/ip.toml, fetched with stdliburllib).available_from_registry()walks the dependency graph to build the resolver's input, so resolution now runs against a registry rather than an ad-hoc scan. TheresolveCLI was refactored ontoLocalDirectoryRegistry, and a newinstallcommand resolves then fetches every pinned core into the cache (--cache-dir, default~/.hdlpkg/cache), verifying each fetched digest against the lockfile (fail closed). The HTTP backend is tested against a localhosthttp.server. ExposedRegistry/LocalDirectoryRegistry/HttpRegistry/available_from_registryfrom the package API; removed the now -obsoletetest_planned_stubs.py. A core's "artifact" is its manifest bytes until M5 packaging defines the packed form (the interface is unchanged by that). Deferred (now Open Non-Blocking Issues): the Git-backed and OCI artifact registry backends — both need external tooling / a live service to build and test, so they could not land honestly within this milestone. Files:src/hdl_ip_packager/registry.py,src/hdl_ip_packager/cli.py,src/hdl_ip_packager/__init__.py,tests/integration/test_registry.py,tests/integration/test_resolve_cli.py,tests/unit/test_cli.py.
M3 — Content-addressed cache — June 2026¶
- [x] Implemented the content-addressed local cache (
cache.py).ContentAddressedCacheis a blob store keyed by the SHA-256 of each blob's own bytes, sharded git-style (<root>/sha256/ab/cdef...). The defining property is verify-on-read:get()recomputes the digest and raisesRegistryErrorif it disagrees with the requested key, so a corrupted or tampered blob fails closed instead of poisoning a build.put()is atomic (temp file +os.replace) and idempotent (content-addressing dedupes);has()/path_for()round out the surface, and digests are validated against the canonicalsha256:<hex>form.default_cache_root()returns a user-level dir (~/.hdlpkg/cache) so cores are reused across projects offline. Reusedlockfile.sha256_digestand the existingRegistryErrorrather than adding a type. The store is standalone this milestone; M4's registry backends fetch into it and M5's packaging defines blob contents. ExposedContentAddressedCache/default_cache_rootfrom the package API. Files:src/hdl_ip_packager/cache.py,src/hdl_ip_packager/__init__.py,tests/integration/test_cache.py.
Release 0.2.0 — June 2026¶
- [x] Tagged
0.2.0per the Release plan: the first release where the tool does its core job — resolve a dependency graph (M1) to a deterministic, verifiableip.lock(M2) viahdlpkg resolve. Bumpedpyproject.toml+__init__.py.
M2 — Lockfile (ip.lock) + hdlpkg resolve — June 2026¶
- [x] Implemented the lockfile model and wired
hdlpkg resolve. New purelockfile.py:LockedPackage(vlnv + source + checksum) andLockfile(from_resolution,to_toml/from_toml/from_path,verify,matches_resolution). The file is TOML with a schemaversionand a[[package]]array sorted by VLNV, so it is deterministic and diff-friendly;from_toml(to_toml(x)) == xround-trips, andverifyfails closed on a missing or mismatched checksum. Asha256_digest(bytes)helper gives the canonicalsha256:<hex>form. TheresolveCLI command (now real, removed from the planned stubs) loads the root manifest, discovers candidate cores by scanning--searchdirectories forip.toml(a stopgap until M4's registry backends), runs the resolver, and writesip.locknext to the manifest. The recorded checksum currently digests the manifest bytes; M3 widens it to full packaged content without changing the format. ExposedLockfile/LockedPackage/sha256_digest/LockfileErrorfrom the package API and addedLockfileErrorto the hierarchy. Verified end to end on the bundledexamples/(UART resolves the FIFO dep). Files:src/hdl_ip_packager/lockfile.py,src/hdl_ip_packager/cli.py,src/hdl_ip_packager/exceptions.py,src/hdl_ip_packager/__init__.py,tests/unit/test_lockfile.py,tests/integration/test_resolve_cli.py,tests/unit/test_cli.py.
M1 — Dependency resolver — June 2026¶
- [x] Implemented the dependency resolver (
resolver.py).resolve(root, available)turns the root manifest plusavailable: Mapping[PackageRef, Sequence[Manifest]](the manifests of each package's known versions) into aResolution= one concreteVlnvper package. Algorithm: backtracking search that picks the newest version satisfying every accumulated constraint, follows each chosen candidate's own[dependencies]transitively, intersects constraints on shared packages (diamonds), and falls back to older versions when a newest-first choice makes a transitive constraint unsatisfiable. Single version per package, fail-on-conflict (HDL can't elaborate two versions of a module); pre-releases are excluded unless a constraint's operand is itself a pre-release of the same core (reusing theVersionConstraintrule). The module is pure (no I/O) —availableis supplied by the caller, so a registry (M4) can feed it later without changing the contract;ResolutionErrornames the offending package, its constraints, and the versions on offer. Chose to keyavailableon full manifests rather than bare versions so transitive deps are reachable without a separate lookup. ExposedResolution+resolvefrom the package API. TheresolveCLI command stays a planned stub until M2 (lockfile) gives it something to write. Files:src/hdl_ip_packager/resolver.py,src/hdl_ip_packager/__init__.py,tests/unit/test_resolver.py,tests/unit/test_planned_stubs.py.
Release plan + first tag (0.1.0) — June 2026¶
- [x] Defined the pre-1.0 release plan and cut
0.1.0. Added a "Release plan" section to this tracker:0.MINOR.PATCHwhile pre-1.0 (MINOR = capability milestone and may break formats; PATCH = fixes;-rc.Nfor risky cuts), with the insight that the SemVer contract here is the on-diskip.toml/ip.lockformats - CLI rather than the Python API. Releases are cut at capability boundaries, so
milestones are grouped into six minor releases (0.1 foundation, 0.2 = M1+M2,
0.3 = M3+M4, 0.4 = M5, 0.5 = M6, 0.6 = M7) with
1.0.0reserved for M8 plus an explicit stability commitment (frozen formats, stable CLI/registry protocol, a third-party publish/consume, and an rc soak). Bumped the package to0.1.0(pyproject.toml,__init__.py) and tagged it as the first release and a low-stakes shakedown of the newrelease.ymlpipeline. Files:docs/progress_tracker.md,pyproject.toml,src/hdl_ip_packager/__init__.py.
Release automation (tag -> PyPI) — June 2026¶
- [x] Added a tag-driven PyPI release workflow. New
.github/workflows/release.ymlfires on anX.Y.Z(orX.Y.Z-rc.N) tag, builds the wheel + sdist withpython -m build, and publishes them to PyPI via OIDC "trusted publishing" (pypa/gh-action-pypi-publish,id-token: write, apypienvironment) so no API token is stored in the repo. The build job first runs a newscripts/check_release_version.pyguard that fails the release if the git tag disagrees with[project].versioninpyproject.toml, preventing a mislabelled artifact. The guard's comparison logic is pure (it takes the ref and thepyproject.tomltext as arguments) and unit-tested intests/unit/test_check_release_version.py; only itsmaintouches the environment/filesystem. Both files are stdlib-only (tomllib). One-time setup is required on PyPI (register the repo + workflow as a trusted publisher and create thepypienvironment). Files:.github/workflows/release.yml,scripts/check_release_version.py,tests/unit/test_check_release_version.py.
Property-based tests (Hypothesis) — June 2026¶
- [x] Added Hypothesis property tests for
version.py. Newtests/unit/test_version_properties.pyasserts the invariants that must hold for every input, complementing the example-basedtest_version.py: the parse/render round-trip (Version.parse(str(v)) == vand exact string round-trip), that ordering is a genuine total order (trichotomy + antisymmetry) and thatsortedagrees with it, that a constraint built from a version contains/excludes that version per its operator (^/~/=/>=/<=include,>/<exclude, and^excludes the next major), and grammar fuzzing thatVersion.parse/VersionConstraint.parseonly ever raise their declared error type on arbitrary text. A sharedsettings(max_examples=60, deadline=None)keeps the loop fast and sidesteps wall-clock-per-example flakiness on the AV-throttled paths noted inCLAUDE.md. Addedhypothesisto thedevextra. Files:tests/unit/test_version_properties.py,pyproject.toml.
Pre-commit hooks — June 2026¶
- [x] Added
.pre-commit-config.yamlmirroring the CI gates. Contributors can now runpre-commit installto catch ruff (lint + format) and mypy (strict onsrc/) failures ongit commit, before CI. The mypy hook usespass_filenames: false+args: [src]so it always checks the whole library tree exactly as CI does, rather than per-file fragments; tool rules stay inpyproject.tomland the pinned hook revs track the floors in thedevextra. Also wired standard hygiene hooks (trailing-whitespace, end-of-file-fixer, check-yaml/-toml, check-merge-conflict, check-added-large-files). Addedpre-committo thedevextra and atests/unit/test_precommit_config.pyintegrity test that parses the config and asserts the CI-mirroring hooks are present (so a typo can't silently disable the local gates). Files:.pre-commit-config.yaml,pyproject.toml,tests/unit/test_precommit_config.py.
Coverage gate ratchet — June 2026¶
- [x] Raised the coverage
fail_undergate from 85 to 93. With the implemented surface now larger (and theinitscaffolder added), the suite sits at ~96%, so the old 85 floor no longer protected against regressions. Bumpedfail_underto 93 in[tool.coverage.report], keeping a small buffer below the live number so a feature whose tests land in the same change is never tripped by a transient partial branch. Also closed the last gaps incli.pyby covering theinitcommand's interactive-prompt path (both a successful prompt and a blank-answer failure), viamonkeypatchonsys.stdin.isatty+builtins.input. Files:pyproject.toml,tests/unit/test_cli.py.
hdlpkg init scaffolder — June 2026¶
- [x]
hdlpkg initscaffolds a starterip.toml. Added a purescaffold.pymodule: a frozenScaffoldOptionsvalue type (identity validated by reusingPackageRef/Version,topdefaulting to the core name) and arender_manifestfunction that emits a complete, valid manifest with onertlfileset and onesimtarget. The renderer is deliberately I/O-free and its output round-trips throughManifest, so a freshly scaffolded core passeshdlpkg validateimmediately; a unit test asserts that invariant. The CLIinitcommand (now a real command, removed from the planned-stub list) is the thin I/O wrapper: it takes--vendor/--library/--name/--version/--description/--license/--topflags plus an optional target directory, prompts for the three required identity fields only when stdin is a TTY (so CI/tests never block), refuses to overwrite an existingip.tomlunless--forceis given, and writes the file. Rationale: a low-effort, high-value DX win that lets authors start a core without copying an example by hand, and it lands cleanly before the M1 resolver. Files:src/hdl_ip_packager/scaffold.py,src/hdl_ip_packager/cli.py,tests/unit/test_scaffold.py,tests/unit/test_cli.py.
Examples and developer experience — June 2026¶
- [x] Documentation site (MkDocs Material -> GitHub Pages). Added
mkdocs.yml(Material theme, light/dark toggle, search, thedocs/tree as nav) and adocsoptional-dependency group (mkdocs-material). A new.github/workflows/docs.ymlbuilds the site and publishes it to GitHub Pages on push tomainusing the official Pages flow (upload-pages-artifact+deploy-pages, withpages: write+id-token: writeand apagesconcurrency group); it requires the repo's Pages source set to "GitHub Actions". The config is deliberately kept free of!!python/name:tags sotests/unit/test_docs_site.pycansafe_loadit and assert everynavpage exists, catching a renamed/removed doc before it breaks the published site (pyyamladded to the dev extras for that test). Cross -repo links tosrc/and root files render as build warnings (not errors) via thevalidationsettings, so a plainmkdocs buildstays green; making those links resolve on the site is deferred. Files:mkdocs.yml,.github/workflows/docs.yml,pyproject.toml,tests/unit/test_docs_site.py,.gitignore. - [x] Bundled example IP cores under
examples/. Added two real cores with validip.tomlmanifests:acme:common:fifo:1.0.0(a synchronous FWFT FIFO, leaf) andacme:comm:uart:1.2.0(an 8N1 UART whose receive path buffers bytes in the FIFO, so it declares"acme:common:fifo" = "^1.0.0"). Together they form a minimal self-contained two-node dependency graph that will exercise the resolver (M1) once it lands. Each core ships small synthesizable SystemVerilog (rtl/) plus a smoke testbench (tb/) so every fileset path resolves to a real file. A newtests/integration/test_examples.pyguards three properties in CI: every manifest validates (viaManifestand thehdlpkg validatepath), every fileset-referenced file exists on disk, and everyacmedependency points at another bundled example. Rationale: gives the docs concrete cores to point at and replaces inline-only fixtures with on-disk manifests, catching schema drift and dangling source paths automatically. Files:examples/,examples/README.md,tests/integration/test_examples.py.
Project bootstrap — June 2026¶
- [x] Repository, Python project scaffolding, and conventions established.
Created the project structure mirroring the reference project's git/docs/AI
conventions:
README.md,.gitignore,.gitattributes,LICENSE(MIT),CLAUDE.md, and thedocs/set (this tracker,architecture.md,ai_agent_instructions.md,INDEX.md,README.md,research/state_of_the_art.md). Set up asrc/-layout Python package withpyproject.toml(hatchling backend,hdlpkgentry point) and the dev toolchain (pytest, ruff, mypy) configured in one file. Files: repo root,docs/,pyproject.toml. - [x] Foundation modules implemented + tested.
version.py(SemVer 2.0.0 + constraint grammar),vlnv.py(VLNV identity),manifest.py(ip.tomlparse/validate),exceptions.py(error hierarchy),cli.py(hdlpkginfo/validate+ wired planned commands), and planned-subsystem seams (resolver.py,registry.py). 108 tests pass, ~96% coverage; ruff + mypy(strict on src) clean. Files:src/hdl_ip_packager/*,tests/*. - [x] Scalable test framework + reporting. Marker-based pytest layout
(
unit/integration/slow), shared fixtures + a local per-module summary inconftest.py, coverage gate, andscripts/render_test_summary.pythat turns JUnit XML into a foldable GitHub step summary (cross-platform, stdlib-only). Files:tests/,scripts/render_test_summary.py,pyproject.toml. - [x] CI pipeline. GitHub Actions workflow runs the suite on push/PR across
Python 3.11/3.12, enforces ruff + mypy, and renders the test summary. Files:
.github/workflows/ci.yml. - [x] State-of-the-art research captured. Surveyed software package managers
(pip/npm/Cargo/Go/Conda/Docker-OCI) and HDL tools (IP-XACT, FuseSoC, Bender,
Orbit, hdlmake, Vivado IP Packager); recorded findings, a comparison table, and
the nine design decisions they drive. File:
docs/research/state_of_the_art.md.
Archive¶
Empty — the project is new.