This file is the authoritative description of the project's knowledge graph (a Label
Property Graph stored in rmp graph, roadmap gometadata). The file and the graph must
mirror each other — whenever a new label, edge type, or property is introduced, update
both in the same step.
- Provenance: every node and edge carries
gitCommit(full hash of the commit when the element was last confirmed) andgitDate(ISO date, e.g.2026-06-01). - Granularity decision: the project has 1026 tests and 1000 exported functions. These are
not modeled one-node-each (that would be noise). Tests live as a
testCountproperty onPackage(individualTestnodes exist only when a test verifies a trackedFeature). Functions are modeled only for the public API surface. Benchmarks and fuzz targets are modeled individually — they are bounded (60 / 27) and security/performance-relevant. - Source of truth: files are ground truth. On a graph/file mismatch, trust the files, fix the graph, and note the discrepancy.
| Label | Key | Count* | Properties |
|---|---|---|---|
Package |
name |
29 | name, path, module, layer, description, testCount, entryReachable, gitCommit, gitDate |
File |
path |
75 | path, name, package, gitCommit, gitDate |
Type |
name+package |
34 | name, package, file, kind, exported, gitCommit, gitDate |
Function |
name+package |
11 | name, package, file, signature, kind, gitCommit, gitDate |
Feature |
name |
34 | name, description, domain, package, file, spec_ref, type, commit_introduced, commit_fixed, gitCommit, gitDate |
FuzzTarget |
name |
27 | name, package, file, gitCommit, gitDate |
Benchmark |
name |
60 | name, package, file, gitCommit, gitDate |
Spec |
name |
9 | name, standard, ref, domain, gitCommit, gitDate |
Commit |
hash |
12 | hash, short_hash, message, author, date, scope, gitCommit, gitDate |
Test |
name |
~4 | name, package, file, type, description, commit_introduced, gitCommit, gitDate |
FormatCapability |
format |
13 | format, extensions, read, write, exif, iptc, xmp, container, gitCommit, gitDate |
Audit |
key |
4 | key, date, scope, method, baseline, prior_audit, critical, high, medium, low, findings_new, new_tasks, sprints, status, remediation_commits, remediation_date, confirmed_live, closed_verified_fixed, headlines, gitCommit, gitDate |
ConformanceBattery |
name |
1 | name, id, package, file, ruleCount, ruleSections, defectsFixed, status, commitHash, completedDate |
* approximate at bootstrap (402a067, 2026-06-01).
entry (root gometadata) · exif · format · format-container
(jpeg/png/webp/heif/tiff) · format-raw (arw/cr2/cr3/dng/nef/orf/rw2) · internal ·
example.
exif · iptc · xmp · container · api · hardening.
Computed from the graph's own DEPENDS_ON edges: true if the package is the entry
(gometadata) or transitively imported by it; false otherwise. At 402a067 (bootstrap):
20 true, 23 false (8 example alternate-entry packages + the 12-package
exif/makernote dead subtree + 3 internal orphans). After task #89 (dead-code removal,
see below): all 14 dead non-example packages deleted; the false set is now 8 example
packages only. Query: MATCH (p:Package {entryReachable:false}) RETURN p.name.
To make the module's real, code-verified capabilities directly queryable (added
2026-06-03, for the v1.1.0 release), the graph carries one FormatCapability node per
supported container format (13). Each node is the authoritative answer to "what can the module
do with format X" and mirrors the README Supported formats table and format.SupportsWrite:
| Property | Meaning |
|---|---|
format |
container name — JPEG, TIFF, PNG, WebP, HEIF, AVIF, CR2, CR3, NEF, ARW, DNG, ORF, RW2 |
extensions |
recognised extensions (detection is by magic bytes, never by extension) |
read |
metadata read supported — true for all 13 |
write |
metadata write supported — equals format.SupportsWrite: true for all 13 formats. TIFF/DNG: copy-and-relocate (#92/#93/#94; DNG re-enabled after the #98 SubIFD out-of-line value fix). CR2: copy-and-relocate + Canon marker restore (#95). CR3: stco/co64 relocation (#91). NEF/ARW/ORF/RW2: un-gated (#102/#103/#104) with manufacturer MakerNote/SR2 rebasing, real-corpus validated. The only write exception is BigTIFF, which returns ErrWriteNotSupported (BigTIFF read is supported; #107). |
exif / iptc / xmp |
which metadata blocks the format carries on read (iptc only JPEG + TIFF) |
container |
structural family: JPEG / TIFF / TIFF-based / ISOBMFF / RIFF / PNG |
These are standalone nodes (no edges) — consistent with engine constraint #2 (heterogeneous
edges to pre-existing nodes cannot be added incrementally); the format / container
properties carry the linkage. Example queries:
MATCH (c:FormatCapability {write:true}) RETURN c.format // writable formats (all 13)
MATCH (c:FormatCapability {iptc:true}) RETURN c.format // carry IPTC (JPEG, TIFF only)
MATCH (c:FormatCapability {container:'TIFF-based'}) RETURN c.format, c.write // TIFF-based formats (all writable: TIFF, DNG, CR2, NEF, ARW, ORF, RW2)Out-of-scope formats (CRW, RAF, MRW, IIQ, X3F, SRW, PEF, RWL — see doc.go) are intentionally
NOT modelled as FormatCapability nodes: the module returns UnsupportedFormatError for them.
Structural
| Type | Pattern | Meaning |
|---|---|---|
PART_OF |
Package → Package |
directory/structural nesting (e.g. exif/makernote → exif) |
DEPENDS_ON |
Package → Package |
Go import dependency (intra-module only) |
CONTAINS |
Package → {File, Feature, Type, Function, FuzzTarget, Benchmark} |
package owns the element |
DEFINES |
File → {Type, Function} |
file declares the type/function |
Semantic
| Type | Pattern | Meaning |
|---|---|---|
COMPLIES_WITH |
Feature → Spec |
feature implements/conforms to a standard |
FUZZES |
FuzzTarget → {Package, Feature} |
robustness surface under test |
TESTS |
Test → {Feature, Package} |
test verifies the target |
HAS_TEST |
Package → Test |
package owns the (feature-linked) test |
INTRODUCED |
Commit → Feature |
commit that introduced the feature |
FIXED |
Commit → Feature |
commit that fixed/hardened the feature |
RESOLVES |
Commit → Audit |
commit that remediates an audit finding/sprint (×19) |
FOLLOWS |
Audit → Audit |
an audit pass that succeeds a prior one (×2) |
ENABLES |
Feature → Feature |
one feature unlocks another (×1) |
HAS_CONFORMANCE_BATTERY |
Package → ConformanceBattery |
package owns a spec-conformance test battery (×1) |
- The root package is the dispatcher: it imports every container/format package
(
exif,format,format/{jpeg,png,webp,heif,tiff},format/raw/*,iptc,xmp,internal/metaerr) and reconciles results into the unifiedMetadata. formatitself only does container detection (FormatID,Container,SupportsWrite); per-format extraction lives in the subpackages.- TIFF-based RAW formats (
arw, cr2, dng, nef, orf, rw2)DEPENDS_ONformat/tiff, whichDEPENDS_ONexif.cr3is self-contained (BMFF parsed inline; depends on no intra-module package). internal/testutilhas no intra-module production importer (test-only helper); it is kept because it is imported by test files across the module and provides no dead-code risk.- Dead-code subtrees removed in task #89 (2026-06-05):
exif/makernote/(dispatch.go + 11 vendor packages: canon, dji, fujifilm, leica, nikon, olympus, panasonic, pentax, samsung, sigma, sony),internal/bmff, andinternal/byteorder— 14 packages / 33 files deleted. None had any live intra-module importer (confirmed bygrepbefore deletion). The live MakerNote dispatch remains atexif/makernote_parse.goinside theexifpackage. Package count: 43 → 29.
- Post-commit sync:
git diff --name-only HEAD~1 HEAD; for each changed file, update itsFilenode's provenance, MERGE new nodes, DETACH DELETE removed ones, and bump the provenance of touchedFeature/Packagenodes. - Hygiene: label-less or orphaned nodes are data-quality bugs —
MATCH (n) WHERE size(labels(n)) = 0 RETURN count(n)must return 0.
Materialized 296 nodes, 422 edges; zero label-less nodes, zero nodes missing provenance.
| Label | n | Label | n | |
|---|---|---|---|---|
| File | 75 | FuzzTarget | 26 | |
| Benchmark | 60 | Function | 11 | |
| Package | 43 (29 after task #89) | Spec | 9 | |
| Type | 34 | Commit | 5 | |
| Feature | 29 | Test | 4 |
Edges: CONTAINS 235 · DEPENDS_ON 50 · DEFINES 45 · FUZZES 26 · PART_OF 24 ·
COMPLIES_WITH 26 · TESTS 7 · HAS_TEST 4 · FIXED 3 · INTRODUCED 2.
Accepted orphans: 4 Commit nodes (release/history commits not tied to a single tracked
feature — linking them would be fabrication).
Sprint 8 (test-hardening battery, now CLOSED) raised coverage and closed several DoS/robustness gaps across the Tier 1/2 critical packages. Graph after this update: 309 nodes, 428 edges (was 296 / 422); zero label-less nodes; every Sprint-8 commit present exactly once.
Provenance + testCount bumped on the 7 touched Package nodes (and the 7 changed
production File nodes):
| Package | testCount (was → now) | confirmed at |
|---|---|---|
internal/iobuf |
8 → 21 | dcc23f7 (2026-06-02) |
exif |
201 → 216 | b26f020 (2026-06-02) |
format/tiff |
17 → 38 | d79db96 (2026-06-02) |
gometadata |
109 → 130 | 236acb5 (2026-06-03) |
xmp |
70 → 108 | 2330d74 (2026-06-03) |
iptc |
51 → 74 | 3f5768e (2026-06-03) |
format/jpeg |
43 → 84 | af1c9e0 (2026-06-03) |
New nodes: 7 Commit (the Sprint-8 commits), 5 Feature (domain hardening), 1
FuzzTarget (FuzzRead, the root end-to-end target; total fuzz targets 26 → 27).
New hardening Feature nodes (each linked Commit -[:FIXED]-> Feature):
Feature id |
package | commit_fixed |
|---|---|---|
feat:iobuf:pool_safety_caps |
internal/iobuf |
dcc23f7 |
feat:tiff:bigtiff_magic_gate |
format/tiff |
d79db96 |
feat:xmp:document_size_cap |
xmp |
2330d74 |
feat:iptc:encode_receiver_purity |
iptc |
3f5768e |
feat:jpeg:extended_xmp_guid_cap |
format/jpeg |
af1c9e0 |
236acb5 -[:INTRODUCED]-> (FuzzTarget FuzzRead). Edge totals: FIXED 3 → 8, INTRODUCED 2 → 3.
Incremental-edge compromise (consequence of engine constraint #2 below): the 5 new
Feature nodes and the FuzzRead FuzzTarget were attached to their owning packages via the
package property, not a CONTAINS/FUZZES edge — heterogeneous edges to pre-existing
nodes cannot be added via MATCH+MERGE, and a single-CREATE form would duplicate the
existing Package node. Query these by property, e.g.
MATCH (f:Feature {package:'xmp', domain:'hardening'}) RETURN f.name. A future full rebuild
would restore the structural edges. The Commit -[:FIXED/INTRODUCED]-> {Feature,FuzzTarget}
edges WERE created (both endpoints new, via the single-CREATE working pattern).
Orphan commit: b26f020 (exif test-only commit) is tracked as a Commit node with no
feature edge — it added tests/fuzz seeds without a production-code feature. Accepted orphans
are now 5.
Transversal Sprint-8 outcomes (not modelled as nodes; recorded here): the CI workflow
gained a fuzz job running 6 targets (FuzzRead, FuzzParseEXIF, FuzzParseIPTC,
FuzzParseXMP, FuzzJPEGExtract, FuzzHEIFExtract) at 10s -race each; the final
security-auditor pass cleared the sprint with no MEDIUM+ findings open and a bounded
aggregate memory model (~336 MiB worst-case for an adversarial ~260 MiB JPEG). Backlog #54
(full BigTIFF read support) was opened to track the deferred scope from feat:tiff:bigtiff_magic_gate.
A production-readiness re-assessment (commit 84993f0 baseline) returned APTO COM
CONDIÇÕES: build/vet/lint/staticcheck/govulncheck clean, 83.6% coverage, 27 fuzz targets
(~120M execs, 0 crashers), race-clean, single indirect dependency (golang.org/x/text),
zero open MEDIUM+ security findings, and a performance profile meeting the ultra-performance
goal (zero-alloc hot paths confirmed). The one blocking condition was a documentation
defect, fixed in Sprint 9 (task #55):
- The code was already faithful — the write gate (
write.go:88),format.SupportsWrite(format/format.go:66),doc.goandErrWriteNotSupportedall correctly report that the 7 TIFF-based containers (TIFF, CR2, NEF, ARW, DNG, ORF, RW2) are read-only. - Only the docs diverged:
README.md(Supported formats Write columnYes→No¹for those 7- footnote; feature rows; fuzz count
18→27) andCHANGELOG.md(the[1.0.0]"read and write" bullet). Now every format's documented Write capability matchesformat.SupportsWrite.
- footnote; feature rows; fuzz count
Deferred (tracked, not done): full RAW/TIFF write support (epic #33), an optional
WithMaxInputBytes guard (LOW), and per-MakerNote / ORF / RW2 / CR3 benchmark gaps (LOW).
The unreachable exif/makernote/* duplicate subtree, internal/bmff, and internal/byteorder
were subsequently removed in task #89 (2026-06-05).
Graph delta: +2 orphan Commit nodes (the docs: fidelity fix 348704c and this model-sync
commit). Commit 12 → 14; accepted orphans 5 → 7. No new labels, edge types, or
properties — the graph shape is unchanged.
Deleted three unreachable subtrees after confirming zero live importers via grep:
| Subtree removed | Files deleted | Reason |
|---|---|---|
exif/makernote/ (dispatch + 11 vendor pkgs) |
29 | Parallel implementation; live dispatch is exif/makernote_parse.go |
internal/bmff/ |
2 | HEIF/CR3 inline their own BMFF parsing |
internal/byteorder/ |
2 | Code uses encoding/binary directly |
Total: 14 packages / 33 files deleted. Package count: 43 → 29.
Graph delta: 14 Package nodes removed (and their PART_OF/DEPENDS_ON/CONTAINS edges).
The makernote layer value is now obsolete. No new labels, edge types, or properties added.
entryReachable:false set reduced from 23 to 8 (example packages only).
The exif/makernote_parse.go file (package exif) is NOT removed — it is the live
MakerNote dispatch and is entry-reachable via the exif package.
Commits 652eda0, 0ef9ce6, 2c6c9a0, e52dd8f, be69f07, 803070e, 8f5752d,
941c3d1 (all 2026-06-05). This closed the last open backlog and completed epic #33
(structural TIFF/RAW write, Option A) plus added BigTIFF read.
FormatCapability matrix — now ALL 13 formats read=true write=true (mirrors
format.SupportsWrite). The four previously read-only TIFF-based formats were un-gated and
their write + writeNote + provenance bumped to HEAD:
| Format | Task | Write mechanism (writeNote) |
|---|---|---|
NEF |
#102 | Nikon private IFDs (PreviewIFD/NikonScanIFD) + SubIFD relocation; validated real Nikon D70 |
ARW |
#103 | Sony SR2Private(0xC634) decrypt+rebase, MakerNote absolute-offset rebase, IFD0 preview JPEG relocation; validated real Sony DSLR-A500 |
ORF |
#104 | non-standard magic IIRS/IIRO + OLYMP MakerNote absolute-offset rebase; validated real Olympus C5050Z |
RW2 |
#104 | non-standard magic IIU0 + 16-byte GUID header offset-shift + recursive IFD rebase; validated real Panasonic DMC-LX7 |
TIFF/DNG writeNotes updated (real-corpus validated #96; conventional XMP/IPTC tag types
#100; word-aligned offsets #99; BigTIFF read #54). The B2 write gate (isTIFFBased /
ErrWriteNotSupported for TIFF-based) was fully eliminated (#105).
New Commit nodes (8) and new Feature nodes (6) — the Features use the
property-linkage compromise (commit_introduced/commit_fixed/package properties, no
edge — engine constraint #2):
Feature name |
domain | commits |
|---|---|---|
feat:tiff:bigtiff_read |
container | introduced 803070e, fixed 941c3d1 |
feat:raw:arw_write |
container | introduced 652eda0, fixed 0ef9ce6 |
feat:raw:orf_rw2_write |
container | introduced 2c6c9a0, fixed e52dd8f |
feat:tiff:conventional_tag_types |
container | 652eda0 |
feat:tiff:word_aligned_offsets |
container | 652eda0 |
feat:write:b2_gate_eliminated |
api | be69f07 |
BigTIFF read (#54): exif.Parse is now BigTIFF-aware — magic 0x002B, 16-byte
header, 8-byte (uint64) IFD entry counts/offsets, 20-byte entries, 8-byte inline threshold,
types LONG8(16)/SLONG8(17)/IFD8(18), parameterised 32/64-bit IFD traversal in
exif/ifd.go; format.Detect routes BigTIFF; classic path unchanged (153 ns/op, 4 allocs).
New files: exif/bigtiff_test.go, format/tiff/bigtiff_test.go, fixtures
format/tiff/testdata/big_cramps_{le,be}.tif. The Sprint-8 feat:tiff:bigtiff_magic_gate
(reject-and-gate) is now superseded by full read support.
New write-relocation files (in format/tiff, all DEPENDS_ON exif): relocate_arw.go,
relocate_orf.go, relocate_rw2.go (joining the existing relocate.go, relocate_nef.go).
Previously the graph held 43 Package nodes while the filesystem had 29 (21
production + 8 example, per go list ./...): the 14 dead packages deleted by task #89
(exif/makernote + 11 vendor subpkgs, internal/bmff, internal/byteorder) had been removed
from disk but their graph nodes were never deleted (ghost records).
Reconciled via targeted DETACH DELETE (no full rebuild needed — see the refined engine
constraint #1 below). After taking a store backup, the 14 ghost Package nodes and their
48 contained child nodes (15 File, 13 Type, 11 FuzzTarget, 5 Benchmark, 3 Feature,
1 Test) were removed with two filtered deletes:
rmp graph delete -r gometadata -q "MATCH (n) WHERE n.package IN [<14 ghost names>] DETACH DELETE n"
rmp graph delete -r gometadata -q "MATCH (p:Package) WHERE p.name IN [<14 ghost names>] DETACH DELETE p"Post-cleanup integrity (all verified): Package count = 29; label-less nodes = 0;
zero nodes whose .package references a removed subtree; total nodes 453 → 391; edges
intact at 428; sanity traversals (e.g. dependants of exif = format/tiff, gometadata)
correct. The single surviving makernote reference is the legitimate File
makernote_parse.go (the live dispatch in package exif), not a ghost. Package queries no
longer need a dead-subtree filter.
Follow-up to the 2026-06-10 production-readiness assessment. Sprint 33 (roadmap gometadata,
tasks #193–#197) remediated five findings. Knowledge-graph impact (this file ↔ graph reconciled):
- Capabilities matrix corrected (#196): the
writeproperty description and the example queries now matchformat.SupportsWriteand the liveFormatCapabilitynodes — all 13 formatswrite=true(BigTIFF write the sole exception), andiptc=trueonly for JPEG and TIFF (the earlier "JPEG, TIFF, DNG" note was wrong; DNG carries no IPTC on read). - Ghost packages reconciled (#197): see "Graph/file discrepancy — RESOLVED" above. A
filtered
DETACH DELETEremoved 14 ghostPackagenodes + 48 child nodes; engine constraint #1 refined (filtered deletes are safe, unfiltered whole-graph deletes are not). - Model ↔ graph reconciliation: two node labels (
Audit×4,ConformanceBattery×1) and four edge types (RESOLVES,FOLLOWS,ENABLES,HAS_CONFORMANCE_BATTERY) — added to the live graph by the post-Sprint-22 reliability-audit and conformance-battery work but never documented — are now in the Node-labels and Edge-types tables above.
Live graph totals after this update: 391 nodes, 428 edges; 29 Package nodes; zero
label-less nodes. By label: Commit 95, File 60, Benchmark 55, Feature 50, Package 29, Type 21,
Test 20, FuzzTarget 16, Spec 16, FormatCapability 13, Function 11, Audit 4, ConformanceBattery 1.
Non-graph sprint outcomes (code/docs on main): #193 reconciled README/CHANGELOG write
capability with the code + added a doc↔code regression test; #195 fixed a real xmp.GPS() bug
(W3C Geo namespace was not decoded), converted 10 result-masking t.Skip calls to t.Fatal,
embedded CI fixtures so corpus tests no longer skip silently in CI, and added docs/TESTING.md;
#194 raised format/tiff coverage 53.9% → 81.5%.
Discovered empirically during bootstrap. Violating these corrupts the store:
- NEVER run unfiltered
MATCH (n) DETACH DELETE n. It does not delete nodes — it strips their labels and properties, leaving undeletable ghost records and corrupting the append-only WAL. Refinement (2026-06-10, task #197): a filteredDETACH DELETEscoped by label/property (e.g.MATCH (p:Package {name:'x'}) DETACH DELETE porMATCH (n) WHERE n.package IN [...] DETACH DELETE n) is safe — empirically verified by removing the 14 ghost packages + 48 child nodes with zero label-less records and full post-delete integrity. The catastrophic case is specifically the unfiltered whole-graph form. Still take a store backup (cp -r ~/.roadmaps/<rm>/graph graph.bak) before any delete, and verify counts +size(labels(n))=0afterwards. - Heterogeneous edges cannot be added between pre-existing nodes.
MATCH (a:LabelA …) MATCH (b:LabelB …) MERGE (a)-[:R]->(b)silently creates 0 edges whenLabelA ≠ LabelB(every MATCH form tested). It works only for same-label endpoints (e.g.Package→Package, used forPART_OF/DEPENDS_ON). - Working pattern for heterogeneous edges: a single
CREATEdeclaring both endpoint nodes as variables and the edge between them. The whole connected graph is therefore built as one ~75 KBCREATEproduced by the regenerator (kept at/tmp/gen_kg.pyduring the bootstrap session). - Guard rails:
createaccepts onlyCREATE/MERGE(noSET; no pattern predicates inWHERE);updateonlySET/REMOVE;deleteonlyDELETE/DETACH DELETE. The class validator is a case-insensitive substring scan, so a forbidden keyword appearing inside a property value falsely trips it — e.g. acreatewhosemessage/descriptioncontains the word "remove" is rejected (matchesREMOVE). Reword string values to avoidset/remove/delete/match/return/where/detachas substrings (confirmed 2026-06-05: "remove dead gate" → rejected; "drop dead gate" → accepted). - Full-rebuild recipe: reset the WAL (
mv ~/.roadmaps/gometadata/graph/walaside sormprecreates an empty store), run the mega-CREATE, then add the same-labelPART_OF/DEPENDS_ONedges viaMATCH+MERGE.