0.5.0
The theme of this release is that a GEDCOM file is mostly opaque identifiers and
undocumented codes, and every feature below removes a reason to leave the line
you are reading.
The graph panel now draws the family, not the file
GEDCOM stores a marriage as a FAM record that both spouses and every child
point at. Drawn literally that put a nameless join record between every pair of
relatives, made a grandparent four hops from a grandchild, and filled the third
column with boxes that answered no question anyone had asked.
-
Families are collapsed. Nodes are people; edges are the relationships
between them, derived by joining through theFAMrecords and then discarding
them. A spouse and a child are now one hop away, not two. -
Columns are generations, not hop counts. Laid out by hops, a sibling and a
grandparent shared a column because both are two steps from the focus — two
generations side by side, saying something false about the family. Ancestors
now run left of the focus and descendants right, the direction a family tree
is read in. -
Couples sit together, joined by a marriage bar, and every child descends
from one point on it. Two parents fanning independently to four children
makes crossings unavoidable; one line per child from the marriage makes them
impossible. -
Sibling groups hang beneath their parents, and siblings run oldest first
within the group. Nothing in the ordering consults the current view, which
is what stops a column rearranging itself every time the selection moves —
a reader clicking along a row of relatives was watching the row reshuffle
under them.This replaced a barycentre pass that ordered each column against its
neighbours. That reads well on any one drawing and produces a different
drawing every time, because the arrangement is computed from whoever happens
to be on screen. Ordering by birth alone held still but tangled. Neither trade
was necessary: hanging each family beneath its parents makes the two columns
agree by construction rather than by search.Measured over 300 neighbourhoods of Linguist's
Royal92.ged: 68% are drawn
with no crossings at all, 87% with two or fewer, and the ninetieth percentile
is three — against 54%, and a ninetieth percentile of fourteen, before any
of this. Tests hold both the crossing figures and the stability ones. -
Marriages are shown, labelled with their year, alongside parent and child
links. Siblings are drawn only where a family records no parents, since
otherwise they are already two hops apart through one. -
Boxes carry dates —
1901–1975, orb. 1930where only one is known.
A tree full of people sharing a name is unreadable without them. Where the
file records none the box shows the name alone: "Individual" under every name
is a label with no information in it, and a row of them reads as though
something failed to load. -
A family is still shown as itself when it is the record under the cursor,
with its members around it and their roles named. Collapsing is right for
families travelled through and wrong for the one being looked at. -
Ancestors / Descendants / Both buttons, because tracing a line back is a
different task from following it forward, and each is half the graph. -
Sources, notes and media are left out unless
gedcom.graph.includeReferences
asks for them: a well-sourced person cites dozens, and they crowd out the
family the panel exists to show. -
A family is never drawn as a box, not even when it is the record under the
cursor. It is a join, not a person: no name, no dates, nothing to say that its
members do not say better. Putting the cursor in one shows that family — the
couple and their children — and highlights all of them. -
Edges run left to right whichever way the pointer is written, and labels are
placed with a backing plate, nudged apart where two would collide. The gutter
between columns was widened so a label such asMarried 1874fits in it, and
labels are drawn after the boxes: painted before them, a marriage year
ended up half hidden behind the spouse below it. -
Relationships that cross no generation — siblings, citations, and a couple —
are routed down the side of their column. Drawn as a left-to-right curve such
an edge doubled back on itself and dropped its label behind a box.
The details panel
A second view in the same GEDCOM panel, for everything the graph has to
discard in order to stay readable.
- Selecting a person shows what they actually contain: occupation,
residence, the place they were born, notes, citations, identifiers — composed
generically from the registry, so a tag nobody anticipated appears rather than
being dropped silently. Every field jumps to its line when clicked. - With nothing selected it describes the file: the program that wrote it,
the character set, the counts, and the submitter with their address and notes.
None of that is a person or a family, and drawn into the graph the submitter
became a box with no generation and no relationships. An unreferencedSUBM
is found too — PAF-era files carry one that the header never points at, and
Linguist's ownRoyal92.gedis one of them. - Clicking a box in the graph now selects rather than navigates. Reading down
a line of descent means looking at a dozen people in turn, and jumping the
editor to each one loses the reader's place in the file for no benefit.
Navigation is its own gesture, on a button that appears on the box. - Text written across
CONTlines is shown whole — monospace, wrapped, in a
block of its own — whether the record is reached as the file's submitter or
selected in its own right.Royal92.gedrecords the file's provenance as a
twenty-eight line posting from 1992, and only its first line was reaching the
panel; its three-line address fared no better. The parser's reassembly was
never at fault, and a test now proves it: this layer was discarding the rest. - A short list of vendor tags the registry has never covered are named in
English —COMMis PAF's comment field, and it is what carries that
provenance note. The list stays short deliberately: a confidently wrong label
is worse than a bare tag. - A record that is neither a person nor a family no longer appears in the graph
as a lone box with no generation and no relationships. The panel says where to
find its contents instead. - The graph scrolls the selection into view when the neighbourhood is larger
than the panel, on whichever axis actually overflows. - Children descend from the parent whose line is being traced rather than from
the midpoint between the couple. The midpoint is nobody — it floats in the gap
between two boxes, and a reader following a descent cannot tell which of the
two it belongs to.
Added
-
Inlay hints, off the back of that: what each pointer names
(1 FAMS @F1@→John Smith + Jane Doe), what each coded value means
(1 SEX M→male,2 QUAY 3→primary), a language tag's language, and how
old the subject was at an event, computed against their ownBIRTdate. Each
kind has its own setting, because the resolved names are indispensable in an
unfamiliar file and noise in your own. An age carries the verb of its event —
died age 73,married age 24— and every hint is set apart from the payload
so it does not read as though the file itself said1 SEX M male. -
Code lens above each record: its shape in the tree, a clickable reference
count that peeks every pointer to it, and a link into the graph panel. Above
HEAD, the dataset summary the header does not carry — record counts and the
span of years the file covers. -
Document links on
WWW,EMAILand URL-valuedFILEpayloads. -
Per-verb hovers. The rule they are written against is that a hover must
answer the question the line provokes rather than restate the tag:DATE— the weekday for an exact date, what a qualified date is claiming,
and how old the subject was; underCHANorCREA, how long ago instead,
because whether a record is maintained is the actual question there.AGE— the notation in words, cross-checked against the recorded dates.
An age that disagrees with the file's own birth and event dates is flagged.
Nothing else in the format checks the two against each other.PLAC— the jurisdictions labelled fromHEAD.PLAC.FORM, which almost no
tool surfaces, plus coordinates from aMAPand a link to a map service.NAME— split into given name, surname and suffix on the slashes, with a
note when no surname is marked.NCHI— how many children are claimed against how many are recorded, which
is the research question rather than a restatement.- Enumerated payloads across
QUAY,PEDI,RESN,SEX,FAMC.STAT,
NAME.TYPE,MEDI,ROLE,FAMC.ADOPand the LDS ordinance statuses. When
a value is outside its set, the alternatives are listed. LANGresolved through BCP 47,FORMthrough media types in both the modern
and 5.5.1 spellings, and the identifier families (REFN,UID,EXID,
RIN,AFN,RFN) distinguished from each other.- A migration note on any tag the file's version removed, carrying the
replacement —ROMNsays to useTRAN.
-
Cardinality stated in words: "Required, exactly one" rather than
{1:1}. -
The detected version, in the status bar. It governs how every line in the
file is read and is frequently guessed rather than declared, so a reader who
disagrees with the guess needs to see it before anything else makes sense. An
inferred version is flagged with the warning background; the tooltip explains
how it was arrived at and what the file contains. -
Payload shapes checked in the grammar, so a wrong value is coloured as
wrong while it is being typed rather than only appearing as a squiggle:- a pointer-only tag given anything but a pointer (
1 ASSO @I1@ df), SEX,QUAY,PEDIandRESNgiven a value outside their set,NCHI,NMRand the other counts given something that is not a number,AGEandTIMEgiven something that does not fit their notation.
Each rule covers only tags whose payload shape is fixed in every context and
in both generations, and extension values are always let through.SOURis a
pointer underINDIand free text underHEAD, so it is not checked;MEDI
is not, because 5.5.1 wrote its values in lower case and 7.0 in upper. A scan
of the whole fixture corpus finds no line these rules fire on. - a pointer-only tag given anything but a pointer (
-
English names everywhere a tag was shown raw: hover titles lead with the
name and keep the tag alongside, pointer targets read "Points at an
Individual record", completion details carry the name, and misplacement
diagnostics name both structures. Twelve 5.5.1-era tags the registry labels
nowhere are named here.
Fixed
-
A pointer payload with anything after it was silently accepted.
1 ASSO @I1@ dfparsed as text, was never indexed as a reference, and drew no
diagnostic — the payload simply stopped being a pointer and nothing said so.
Every structure the registry types as a pointer now requires its payload to be
exactly@xref@, at every strictness, because it is wrong under any reading of
any version. -
Vocabulary diagnostics never said what they were judging against. "Not a
tag in this version of GEDCOM" asked the reader to take on trust both which
version that was and how it had been decided — and when the answer is "we
guessed from the tags in use", that is exactly what they need to know, because
the right fix may be to correct the header rather than the line. Every such
message now names the version, says whether it was declared, inferred or
defaulted, and links to the specification. -
Hovers on
CHANcopied the date from two lines below instead of qualifying it.
Events read as sentences now, and maintenance dates say how long ago. -
The extension could not load in the web extension host at all — the same
class of failure as 0.4.0's, in the other host, and found the same way: by
writing the test that could see it. The worker host decides a module's kind
withpath.endsWith('.mjs') || (extension.type === 'module' && !path.endsWith('.cjs'));
and then refuses ESM outright, because it supports none. This repository is
"type": "module", sodist/browser/extension.jswas read as ESM and threw
before activation. The browser entry is now.cjs, matching the Node one. The
server bundle stays.js: it is passed tonew Worker()by URL, and a worker
script must be served with a JavaScript MIME type. -
Hovers showed the registry's internal type URIs. A structure taking plain
text was described asXMLSchema#stringand a date astype-DATE_VALUE. Every
payload type is now given as a description with an example where one helps, and
a test iterates the whole registry so a newly-added type cannot slip through
undescribed. -
relativeTimecounted two calendar years spanning a leap year as one, because
730 days divided by a mean year length floors to 1. It now counts calendar
years and months. -
The age on an event hover named the event by its tag rather than its label,
readingCENS at 9 years old. The label is keyed by the registry slug, and the
slug forCENSinside anINDIisINDI-CENS.
Testing
- Integration tests in the web extension host, headless via
@vscode/test-web(vp run test:web), covering activation in the worker, the
language server answering from a nested worker, and the graph panel resolving
under the browser content security policy. This closes the last host that
shipped without automated coverage — and found the activation bug above on its
first run. - Tests for every new module, including one that walks the whole registry and
fails if any payload type reaches a reader undescribed, and one that scans the
entire fixture corpus for a line the new grammar rules wrongly reject. - The web harness pins
--quality=stable.@vscode/test-webotherwise fetches
whichever Insiders build is newest that day, so the same commit passes or fails
depending on when it runs — and a broken Insiders build hangs before invoking
any test module, printing nothing at all. Each web test is also raced against
a clock, because a runner that hangs silently is the worst failure mode there
is: it looks exactly like an environment problem and names no line.
Changed
- Dependencies. Every package is at its latest published version, with two
deliberate exceptions, both now enforced by tests rather than left to memory:@types/vscodestays at~1.91.0, matchingengines.vscode. It decides
which APIs the compiler believes exist, so raising it alone would compile
calls to APIs missing from the oldest editor the extension claims to run on
— a failure that lands on a user at runtime. The published types are twenty
releases ahead; nothing here needs them.serialize-javascriptanddiffare pinned throughoverridesto versions
the advisories are fixed in. Both reach the repository only through
@vscode/test-cli→mocha, neither ships in the extension, and neither
could be fixed by an upgrade — which is why the security job failed on every
run.npm auditis now clean. Both replacements were checked against the
code that uses them, including mocha's failure-diff rendering, which a green
suite never exercises.
devEngines.packageManagertakes a range (>=11) and warns rather than
failing. It pinned npm to one exact patch version, which no CI runner happens
to have, sonpm ciaborted withEBADDEVENGINESbefore doing anything. The
real requirement was always "a modern npm".vp run buildis the build task. It wasbundle, which nobody guesses.- F5 now runs it first. The extension host loads the bundles in
dist/, so launching without a rebuild looked exactly like a code change
having had no effect.