Skip to content

✨ Summary link types, and copy / collect on a link type: a chain's far ends as links - #2418

Merged
chrisjsewell merged 26 commits into
masterfrom
claude/vibrant-allen-o22lej-summary
Oct 30, 2026
Merged

chrisjsewell merged 26 commits into
masterfrom
claude/vibrant-allen-o22lej-summary

Conversation

@chrisjsewell

@chrisjsewell chrisjsewell commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Read by the data-model side before going out of draft: needs.json's "summary": true marker and the exchange-contract sentence fit, and nothing collides with its in-flight work. The ubCode side is useblocks/ubcode#4452; this PR merges first.

What

packages/sphinx-needs (+ one sentence in packages/ub-needs-exchange's exchange contract, and a reader test).

  1. The stance. summary = true on a link type's needs_links entry (any link type, authored or derived) says its links materialise a path,
    not an edge of the trace. A summary link type is read where it is named (a filter, a needtable column, a rule's over / field, a needflow's
    :link_types:, a schema rule, a need type's back_links count, which counts its computed links too), and a need holds its links and back links
    as for any link type. It is left out of every view that reads all link types:
    in sphinx-needs, the needflow default (without :link_types: a summary link type is neither drawn nor walked from :root_id:, and the
    needs_flow_link_types fallback leaves it out too). A union cannot name one as a member (needs.derive_invalid). summary beside the deprecated
    copy = true is reported and the copy ignored, so a summary link never leaks into links. needs.json's schema marks the link type and its back links
    "summary": true, and ub-needs-exchange passes it through. The stance is one per name: a need type's entry giving it is a needs.config finding.

  2. The kinds. On a link type, copy takes field = "id" and over, and links each need over names. collect takes field (a link type, a derived
    one included) and over, and gives the links field holds on each need over names. Both take where (c.is_a(...) included), and both may be
    declared on need types alone ([needs.types.<t>.links.<l>]). Each link appears once by target and part,
    its condition dropped. A dead link is carried and reported where it is written, rendered as that link type allows, and does not mark the needs below it
    as having dead links. Such a link type is a summary implicitly. over may name a union, read through its members, so a chain can climb a trace_up-style
    union rung by rung. What the link step cannot wait for is refused at load:

    • an over naming back links;
    • a collect of its own link type over a link type another rule computes (it would read itself on every need);
    • a field naming a value field or back links;
    • a copy of a field other than id;
    • a select other than "list";
    • after;
    • allow_dead_links beside such a rule.

    Per need, a path is ordered after the needs it reads; needs reading one another's chain around a ring (through a union too) are needs.derive_cycle,
    and only those. The density guard counts these links; fingerprints leave them out.

  3. Incremental builds. A collect on a link type links needs its need does not name (a chain's far ends), so it is classified as a links rule
    is: in a project with one, every change of a need re-writes every need-relevant page. A copy over a link type written in the needs links only the
    needs its own links name, and stays a read of its links (a copy over a computed link type is classified as unnamed too).
    ENV_DATA_VERSION is 19 (17 is 🐛 A need with :hide: is located as a visible need in the same place is #2415's, 18 ✨ NeedItemSource: one class for where a need comes from, and add_need is deprecated in favour of create_need #2419's): a copy / collect on a link type, refused by 9.1.0, now computes, and summary now changes a
    needflow's default, with the configuration unchanged.

Why

Closes #2370.

The far ends of a chain (the requirements a test result is evidence for, three hops up) were only expressible as a field: strings, with no back links
("the test results that reach me"), no :need: rendering, no direct relationship in a graph consumer. The link form adds them. The stance it needs, that such a link
is a summary and not a trace edge, applies to authored and other derived link types too.

Bringing it up to master. The branch was built on the derive branch, since squash-merged as #2384; master was merged in (merge commits, no rebase).
The conflicts kept both sides: the changelog's Unreleased bullets, LinkSchema's severity (#2395) beside summary in the basic-doc snapshot,
a path's select default, and the corpus (master's where-follows-inherits where master has it, this branch's cases after it). Checked against what master
gained since: the incremental rules of #2414 (a collect on a link type was read as an ordinary over and left the old far end's page stale: fixed,
red first, with scratch-vs-incremental cases for both kinds); a [needs.types.<t>.back_links.<l>] count (#2395) naming a summary or computed link type
(allowed, counts every link, pinned); c.is_a (#2401) in a path's where (pinned in a test and the corpus); a path declared on need types alone and a need
type's summary (#2400, pinned); the needs.json writer's nullable rule and "" -> null (#2395, #2400: fields only, beside summary on link types), and
the needflow change on master (a view category only). Later merges brought in #2415 (hidden-need location) and #2419 (NeedItemSource), which took
versions 17 and 18, so this branch's bump is 19; every bump stays in data.py's version notes and in the changelog's bump bullet.

Corpus. derive_conformance.toml gains twelve cases on top of master's 92 (104, sha256 c4721bcc6cb1532b4a39a79c3ba5325c75b006762ca5d25a5e1c78809923ba01;
python_only 2, ubcode_only 1) and an optional schema.<name> expectation, documented in the header; master's cases are untouched.
ubCode re-vendors it and mirrors the stance and the kinds in useblocks/ubcode#4452, including its own consumers that read all link types:
the needflow default, the agent walks, ubc diff's default, the Need Viewer tree and the Cypher schema flag.

Follow-up, not in this PR. summary on a need type's own link entry is a needs.config finding today, so an authored link type declared on
need types alone cannot be a summary. It can be lifted under the one-name-one-shape rule (#2284): accept it there when every need type declaring
the name agrees, and report a disagreement as any other shape mismatch.

Tests

Checklist

  • I wrote this change myself and have read every line of it; it was not generated automatically from an issue.
  • I ran the package's tests (uv run poe test-needs, uv run poe test-ub-needs-exchange) and they pass.
  • Documentation is updated where behaviour or options change.
  • The package's docs/changelog.rst has an entry under Unreleased.
  • uv run poe lint and uv run poe typecheck pass.

…every NaN one value

`sphinx_needs_testkit.conformance.same`, which holds a build's value to a
pin in the derive and field-declaration runners, compared floats with
`==`, so the `-0.0` pin of `fingerprint-defaults-compared-as-text` could
not tell a build that kept the sign from one that dropped it, and a NaN
pin could never pass. A float is now compared by its IEEE 754 bits, with
every NaN mapped to one pattern, as ubCode's runner compares
`f64::to_bits`; ints keep `==` and the type check, bools stay `is`.

`tests/test_testkit_conformance.py` pins it (red first: 7 failed against
the old `same`), and with the fix, editing that case's pin to `0.0` makes
the case fail (`REQ_1.zero: -0.0, expected 0.0`). Both corpora pass
unchanged.

Closes #2216.
…a derive rule, a call or variant, or both

The `needs.derive_scope` report at a `needextend` whose filter names a
field computed later said "which a dynamic function or variant computes"
also for a field a declared `derive` / `derive_by_type` rule computes,
where the project wrote no dynamic function or variant. It now takes
ubCode's three-way clause (`derive_scope_extend_filter`): "a derive rule",
"a dynamic function or variant", or "a derive rule, a dynamic function or a
variant" when the filter names fields of both. On one need a rule wins
over a call on its (closed) field, as in ubCode. Only the "which ...
computes" clause moved; the rest of the sentence, the code and the
severity are unchanged.

The two tests pinning the old text move with it, and a new test pins all
three wordings in one project; against the old wording the three are red.
Changelog: a new `Unreleased` section.

Closes #2207.
ubCode has run the case since useblocks/ubcode#4038, through its runner's
own `run_case`, by a dedicated test that also asserts the flag is still
set, so the re-vendor of this file fails loudly there and says what to do.
Re-measured now with ubCode's debug CLI on the case's project: every
pinned value is the same in both tools and neither reports a contract
finding (ubCode's `derive_sphinx_minimum` notice aside). The case runs in
sphinx-needs either way; nothing else changes here. Two cases stay
`python_only` (`union-over-a-member-computed-last`,
`union-computed-last-read-in-its-step`).

Closes #2214.
Six additions to `derive_conformance.toml`, four new cases and two edits
(84 -> 88 cases, `EXPECTED_CASE_COUNT` with it):

- `links-include-parts-of-imported-and-external-needs` (new): the parts
  of a `needimport`ed need (those its parsed content gives) and of a
  `needs_external_needs` need (those its record's `parts` give; its
  content is not parsed) are `include_parts` candidates of a `links` rule.
- `filter-inline-spellings` (edit): `links_from_filter(...,
  include_parts=True)` links a part after its need, and `copy(filter=...)`
  takes needs only, never a part.
- `fingerprint-nested-need` (new): a nested need's content and fingerprint,
  and its parent's; `parent_needs` does not enter.
- `fingerprint-computed-tags-default` (new): a `[[...]]` default of `tags`,
  computed per need, compared with `[]` by the fingerprint.
- `fingerprint-computed-link-default-from-global-options` (new): a link
  type's `[[...]]` default through `[needs.global_options]`.
- `by-type-invalid-shapes` (edit): `entry_not_table` and `entry_invalid`
  declare `default = 7`, and an `impl` need is added, so the placeholder
  (null, on the invalid entry's type) and the default (7.0, on a type
  with no rule) are different values the case tells apart.

Every pin was measured now in BOTH tools on the case's project: this tree
with the `needs` builder, and ubCode's debug `ubc_cli` (0.38.0) through
`build needs` / `build index`, read by its runner's rules (an uncarried
link type reads as `[]`); all pins equal, and the contract findings the
same multiset. One difference was found and is deliberately not pinned:
an imported record's own `parts` key where its content gives no part
(sphinx-needs takes the content's parts, none; ubCode the record's). A
comment in the new case says so.

Closes #2215.
…n upwards

A new subsection under the `copy` kind, "Starting a list roll-up one rung
up" (`needs_derive_rollup_rung_up`): `req_ids` starts on the
specification as a `copy` of `id` over `satisfies` with `select =
"list"`, and is collected on each rung below (implementation, test). One
sentence says why the list does not start on the requirement itself: a
plain `copy` of `id` reads one string, which an array field does not
hold, and `select` takes a list only with `over` or `join`; both
spellings are refused (`needs.derive_invalid`). The same chain read
upwards gives each need the tests that reach it (`reached_by`: `collect
id over tests_back` on the implementation, then `collect reached_by` over
each rung's back links). The `collect` section points to it.

Both examples, in the per-type spelling the page uses, and the two
refused spellings, were built in this tree and with ubCode's debug CLI:
every value and both refusals are the same in the two tools. The page
was checked with a dummy Sphinx build of it alone (no markup errors); the
full docs build was not run. Changelog: a Documentation entry under
`Unreleased`.

Closes #2379.
`all` over no candidate is `true`, which on a requirement nothing tests
reads as "every test passed". A rule of kind `any` or `all` now takes
`empty = true|false`, the value it gives there instead. No candidate is
the candidate list once `where` has dropped the needs it does not hold
on (a requirement that only a specification names, under `where =
"type == 'test'"`, has none). Over candidates `empty` is not read, and a
candidate leaving a boolean `field` unset still reads as false.

Unset, `empty` is the kind's own value over no candidate, `all` `true`
and `any` `false`, so no existing rule changes value. It is a boolean
role (`_BOOLEAN_ROLES`): on any other kind the rule is refused as "kind
'<kind>' does not take the role 'empty'", and a non-boolean value as
"kind '<kind>': the role 'empty' must be a boolean"
(`needs.derive_invalid`).

Corpus: a new case `any-all-empty` (all / any x unset / true / false,
with a test and with a boolean field; a `where` that empties the
candidates, no link at all, and candidates on which `empty` is ignored),
and two refusal rows in `derive-invalid-shapes` (`empty` on `sum`, a
non-boolean `empty`). 88 -> 89 cases. The new case was red against the
unfixed tree (30 value mismatches), and reading `empty` before `where`
(over the raw `over` list) turns it red again.

Docs: the any/all section (an example, `versionadded:: 9.2.0`), the
kinds table, the roles table (`empty` row, and the `test` row points to
it), and the "No candidate" bullet of "How rules are computed".
Changelog: a ✨ entry under `Unreleased`.

Closes #2377.
A `copy` with `over` or `join` now takes a `where`, which narrows its
targets as it narrows the candidates of the other kinds and of `links`
with `join`. It is tested on each candidate alone, BEFORE `select`:
`"first"` takes the lowest-id target that passes `where` and sets the
field, `"unique"` counts (and reports) only those, and `"list"` keeps
their values (in written order over `over`, a need named twice read
twice; in need-id order over `join`). A candidate the filter cannot be
evaluated on (a field it lacks, a type error) is dropped without a
warning, as `_passes` drops it for every other kind.

A `copy` of the need's own field, or of the need `from` names, reads one
need and still takes no `where`, now refused as "kind 'copy' takes the
role 'where' only with 'over' or 'join'" (`needs.derive_invalid`), in
the wording of the existing `select` refusal.

`copy` over a link type already filtered its candidates through `where`
(the role was simply refused); the join branch of `execute_rule` now
filters the matches before splitting into `links` and `copy`. The order
of the pass needed no change: `_RuleReads.read` already adds the names
of a `where` as columns in the join branch for any kind, and as reads on
each target in the `over` branch.

Corpus: `copy-join-where` and `copy-over-where` (first / unique / list
with a `where` that drops the lowest-id setter; a `where` that cannot be
evaluated on that setter; a `where` dropping every candidate), and two
refusal rows in `derive-invalid-shapes` (own-field and `from` copies
with a `where`). 89 -> 91 cases. Both new cases were red against the
unfixed tree. Their `where` reads a field computed in the same step,
declared last, on candidates whose ids sort after the reader's: dropping
the `where` reads from `functions/order.py` (in either branch) turns the
matching case red, so the cases fence the order too.

Docs: the `copy` section (an example, `versionadded:: 9.2.0`), the
join section's "links and copy" and "The order" bullets, the kinds and
roles tables (`where`, `select`), the "Candidates" bullet and the list
of refusals. Changelog: a ✨ entry under `Unreleased`.

Closes #2246.
A link type whose `needs_links` entry says `summary = true` materialises a path rather than
an edge of the trace (#2370). It is read where it is named -- a filter, a needtable column,
a rule's `over` or `field`, a needflow's `:link_types:`, a schema rule -- and a need holds
its links and back links as any link type's; it is left out of every view that reads all
link types, which in sphinx-needs is a needflow without `:link_types:`: neither drawn nor
walked from `:root_id:`.

- a `union` cannot name a summary link type as a member (`needs.derive_invalid`, the
  union holds no links): a union is read as one set of edges
- needs.json's schema marks the link type and its back links `"summary": true`;
  ub-needs-exchange passes it through (contract section 7.1 says so, a reader test pins it)
- the stance is one per name: a need type's `links` entry giving `summary` is a
  `needs.config` finding and the key is ignored; in a rule it is no role
  (`needs.derive_invalid`); a value that is not a boolean is a `needs.config` finding
- the derive corpus gains an optional `schema.<name>` pin (the testkit's `check_schema`,
  as the field-declaration corpus has) and two cases, `summary-key-shapes` and
  `union-refuses-summary-member` (93 cases); ubCode owes a re-vendor
…s as links

`copy` of `id` over a link type, and `collect` of a link type over a link type, declared on
a link type (#2370): `{kind = "copy", field = "id", over = "satisfies"}` on a spec and
`{kind = "collect", field = "chain", over = "implements"}` on the rungs below give every
need of the chain the requirements it traces to as links, and each requirement the needs
that reach it as back links -- what the field form gives as id strings.

- each link once by its target and part, the condition dropped; a dead link is carried
  and reported where it is written (`LinkSchema.path_on`, asked beside `union_on`)
- such a link type is a summary; `summary = false` beside the rule is refused
  (`needs.derive_invalid`) and ignored, and a union cannot name it
- computed with the link types (stratum 1): an `over` naming back links or a derived
  link type, a `field` naming a value field or back links, a copy of a field other than
  `id`, `select` other than "list", `after` are refused at load, not per need
- per need, a path reads its `field` on each need `over` names, so collect-over-collect
  is ordered like an own-field roll-up and a ring of needs is `needs.derive_cycle`;
  where a call computes the need's own `over` in stratum 1 the path waits for it and
  reads the field through its column (as check_linked_values does)
- the density guard counts the links; fingerprints leave them out, as every derived
  link type
- corpus: `link-copy-id-over`, `link-collect-chain`, `link-collect-cycle`,
  `link-kinds-refusals`, `link-collect-dense` (98 cases), header rule added; ubCode
  owes a re-vendor
- two tests that used `copy` as the example of a value kind on a link type now use `sum`
…e written, no copy

- `over` may name a derived link type: a `union` is read through its members
  (`_RuleReads._static_targets`, recursively), so a `collect` of its own link type over
  `trace_up` is computed rung by rung and a ring through the union is exactly its
  members' `derive_cycle`s. Over a link type another kind of rule computes (or a union
  with such a member) the path reads its field through its column: a `copy`, or a
  `collect` of another link type, is computed; a `collect` of its own link type would
  read itself on every need and is refused at load with that reason
- a dead link a copy or collect carries sets `has_dead_links` only on the need writing
  it; its `forbidden` class follows the link type it is written in, rung by rung
  (`_forbidden` walks the path's `over` to the writer); `allow_dead_links` beside a
  copy or collect is refused and ignored, as beside a union
- `copy = true` beside `summary = true` is a `needs.config` finding, and nothing is
  copied into `links`
- the `needs_flow_link_types` fallback leaves summary link types out too
- tests: the own-over wait is pinned by link types sorting before `implements`; the
  forbidden class is pinned in html with both `allow_dead_links` values
- corpus: `link-kinds-refusals`' over_derived is the still-refused shape (a collect of
  its own link type over a `links`-derived link type); four cases appended
  (`link-collect-over-union`, `link-collect-ring-through-union`,
  `link-collect-dead-links`, `link-path-where-reads-later`), 102; header: the schema
  pin's path and a missing pinned key, derived overs, has_dead_links, derive_scope of a
  later `where`. ubCode owes the re-vendor of this revision
master squash-merged the derive branch this one stacked on (#2384), and has since
gained per-type declarations (#2395), extension types (#2400), c.is_a (#2401), the
incremental rewrite rules (#2414) and more. Conflicts kept both sides: the changelog's
Unreleased bullets; LinkSchema's severity beside summary in the basic-doc snapshot;
a path's select default in derive.py; the derive corpus gains master's
where-follows-inherits case ahead of this branch's eleven, 103 cases.
…elevant page

A collect on a link type links a chain's far ends, needs the collecting need
does not name, so its targets are not known before the needs are resolved:
a changed over left the old far end's page showing the need in its back links.
It is now classified as a links rule is (every page holding a need is re-written
on a change), and so is a copy on a link type over a computed link type; a copy
over a link type written in the needs links only the needs its own links name,
and stays a read of its links. Scratch-vs-incremental cases for both kinds.
…here, rules on need types alone

A need type's back_links count names a summary link type as any other and counts
its links, those a copy or collect computes included; a copy or collect may be
declared on need types alone (the link type a summary all the same) and read
c.is_a in its where; a need type's summary stays a needs.config finding.
…mmary is read

A copy or collect on a link type, refused by 9.1.0, now computes, and a link
type's summary now changes a needflow's default, with no change to the
configuration: an upgraded build re-reads every document once, rather than
keeping the empty link lists and old diagrams on the pages it does not re-read.
… on a link type reads c.is_a

The where of a path reads c.is_a as master's where-follows-inherits pins it for a
value rule: with spec inheriting req, a copy keeps the spec, and a collect whose
where refuses a req leaves the spec's chain out. 104 cases.
master's unknown-key report (#2388) names the keys of NeedLinksConfig and
LinkOptionsType, and pins them: this branch adds summary to both.
@github-actions github-actions Bot added pkg: sphinx-needs The sphinx-needs distribution (packages/sphinx-needs): its code, tests and docs pkg: ub-needs-exchange Concerns the ub-needs-exchange package (packages/ub-needs-exchange): the needs.json format record labels Oct 30, 2026
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.10983% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.85%. Comparing base (c1562e0) to head (d68c99b).

Files with missing lines Patch % Lines
packages/sphinx-needs/src/sphinx_needs/derive.py 97.26% 2 Missing ⚠️
...s/sphinx-needs/src/sphinx_needs/functions/order.py 96.55% 1 Missing ⚠️
...ages/sphinx-needs/src/sphinx_needs/needs_schema.py 88.88% 1 Missing ⚠️
...hinx-needs/src/sphinx_needs/roles/need_outgoing.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2418      +/-   ##
==========================================
+ Coverage   94.84%   94.85%   +0.01%     
==========================================
  Files         146      146              
  Lines       25790    25943     +153     
==========================================
+ Hits        24460    24609     +149     
- Misses       1330     1334       +4     
Flag Coverage Δ
ai-index 100.00% <ø> (ø)
codelinks 96.31% <ø> (ø)
mounts 94.27% <ø> (ø)
pytests 94.70% <97.10%> (+0.02%) ⬆️
reports 97.04% <ø> (ø)
ub-test-reports 91.05% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@chrisjsewell
chrisjsewell marked this pull request as ready for review October 30, 2026 16:21
@read-the-docs-community

read-the-docs-community Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Documentation build overview

📚 sphinx-codelinks | 🛠️ Build #35528523 | 📁 Comparing d68c99b against latest (13ee63b)

  🔍 Preview build  

1 file changed
± changelog.html

@chrisjsewell
chrisjsewell merged commit 8dcf332 into master Oct 30, 2026
44 of 45 checks passed
@chrisjsewell
chrisjsewell deleted the claude/vibrant-allen-o22lej-summary branch October 30, 2026 16:47
chrisjsewell added a commit that referenced this pull request Oct 31, 2026
A link type declared on need types alone can now be a summary. Mark it
on the entry of the first need type that declares it:

```toml
[needs.types.result.links.covers]
summary = true
```

It is then a summary on every need type, exactly as `summary = true` on
a `[needs.links.covers]` entry makes it: a needflow without
`:link_types:` leaves it out, and the needs.json schema marks it, and
its back links, `"summary": true`. Until now that key was reported
(`needs.config`) and ignored, so such a link type could be a summary
only when a `copy` or `collect` computed it.

`summary` follows the one-name-one-shape rule that the link type's other
keys (`incoming`, `copy`, `allow_dead_links`, the styles) already
follow:

- **Agreement.** The first need type declaring the link type sets
`summary`, and its silence means `false`. Every other need type may
repeat the value or leave it out. A need type giving another value is
reported (`needs.type_conflict`) and its entry ignored.
- **Computed link types.** A link type that a `copy` or `collect`
computes is a summary in any case. That means a derived one, with the
rule in a declaration that is used; a refused declaration computes
nothing. A declaration may say so, and `summary = false` beside such a
rule is reported (`needs.derive_invalid`) at that need type's entry and
ignored, whichever need type writes it. The rule still applies on that
type. This is what a `[needs.links]` entry already does.
- **Link types declared in `needs_links`.** The `[needs.links]` entry
keeps the stance. A need type's entry giving `summary` is the usual
shape-key warning (`needs.config`), even when it repeats the link type's
own value.
- **`copy = true` beside `summary = true`.** On a link type declared on
need types alone, this is now reported at the need type's entry
(`needs_types['spec'].links['mitigates']`). Nothing is copied, so it is
no longer also reported as deprecated.

This amends the unreleased "Summary link types" entry (#2418); it is not
a new change on its own. `ENV_DATA_VERSION` goes to 20, because the key
now changes a needflow's default under an unchanged configuration.

The shared corpora carry the rule:

- `field_declaration_conformance.toml` gets five cases (agreed,
disagreeing, a copy in a refused declaration, implied by a copy, beside
a `[needs.links]` entry), and its count goes from 123 to 128.
- `derive_conformance.toml` gets one case, `summary = false` beside a
copy on the first and on a later declaration, and its count goes from
104 to 105. A first declaration's ignored `false` sets nothing, so a
later one may still say `true`.
- Both headers now say `summary` is a shape key.

ubCode vendors both files, so this owes it a re-vendor and the matching
change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: sphinx-needs The sphinx-needs distribution (packages/sphinx-needs): its code, tests and docs pkg: ub-needs-exchange Concerns the ub-needs-exchange package (packages/ub-needs-exchange): the needs.json format record

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ A derived link type by copy / collect over links: the far ends of a chain as links, not strings

1 participant