Skip to content

feat(core)!: keep reading a manifest that declares a usage kind we do not know - #557

Closed
vytautas-astrauskas-sensmetry wants to merge 3 commits into
mainfrom
feat/unknown-usage-kind-2
Closed

vytautas-astrauskas-sensmetry wants to merge 3 commits into
mainfrom
feat/unknown-usage-kind-2

Conversation

@vytautas-astrauskas-sensmetry

Copy link
Copy Markdown
Collaborator

.project.json deserializes its usage array through an untagged enum with
no catch-all, so an entry matching no known kind fails the whole document.
The day a fourth usage kind ships, every sysand already installed stops being
able to read those projects at all — not only lock and sync, but info,
add and remove too, and with serde's own wording:

data did not match any variant of untagged enum InterchangeProjectUsageG

A version number does not reach a file format (see the PR below this one), so
that behaviour is not something a major bump can fix later. It has to be
fixed before the release, not after.

What changed

InterchangeProjectUsageG::Unknown(UnknownUsage), which keeps the
entry's JSON verbatim, and is refused in validate instead of at parse time:

`.project.json` declares a usage this sysand cannot interpret, with keys "foo", "bar";
it is either a usage kind from a newer sysand, in which case upgrading
sysand will resolve it, or a malformed entry

Tolerating the entry at the parse boundary and refusing it where a caller
acts on it as a dependency separates the two things the old behaviour
conflated. A manifest stays readable and editable: add, remove and info
leave the entry alone and write it back untouched. An unknown dependency
still can never be silently resolved around: lock, sync, build and
publish refuse by name.

The catch-all matches a JSON object, so a usage entry that is not one
remains a parse error — no future kind can explain that shape. It is the last
variant, as untagged requires.

Python gains InterchangeProjectUsageUnknown (a plain dict) and
InterchangeProjectUsageAny for reading, which is what
InterchangeProjectInfo.usage now holds. Writing still takes
InterchangeProjectUsage, restricted to the kinds sysand understands, so
add(usage=…) cannot be used to declare one.

RELEASE.md replaces the gap recorded in the PR below with what now
holds, including the part that does not change: builds released before this
one still fail to parse such a manifest. This only moves the floor. A fourth
usage kind can be introduced once those builds are out of circulation, not
today.

Breaking

  • InterchangeProjectUsageG gains a variant, so a match over it in Rust
    needs a new arm. The crates are not published, so this is internal.
  • Python: InterchangeProjectInfo.usage is typed InterchangeProjectUsageAny
    on the way out. Code that reads a usage as one of the three known shapes
    needs a check for the unknown one — which is the point: that value can now
    appear where the call used to raise.
  • No file format change. Nothing new is written; a manifest sysand produces
    is byte-identical to before.

Signed-off-by: Vytautas Astrauskas <vytautas.astrauskas@sensmetry.com>
Signed-off-by: Vytautas Astrauskas <vytautas.astrauskas@sensmetry.com>
… not know

Signed-off-by: Vytautas Astrauskas <vytautas.astrauskas@sensmetry.com>
@andrius-puksta-sensmetry

andrius-puksta-sensmetry commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

A manifest stays readable and editable: add, remove and info
leave the entry alone and write it back untouched.

I don't want to rely on this; manifest should be validated on any command that reads it, and I consider it an oversight that we don't do it already.

To achieve the goal of this, PR, I suggest:

  • accept anything for deserialization
  • on validation:
    • warn about unknown keys (for simplicity, ignore if extra keys are present for a known usage type; keeping track of them would require a rather large refactoring)
    • use Unknown usage variant if they are not extra keys for a known type
  • error out if Unknown usage kind is present when resolving or modifying dependencies (otherwise we can't ensure no duplication)

Comment thread core/src/model.rs
Comment on lines +50 to +57
// The catch-all `Unknown` variant is what keeps `.project.json` readable
// across versions: a manifest declaring a usage kind only a newer sysand
// knows still parses, so every command that does not have to interpret that
// usage keeps working and rewrites it untouched. Interpreting one is refused
// in `validate`, so an unknown dependency can never be silently dropped.
//
// It must stay the last variant: `untagged` tries variants in order and this
// one matches any JSON object.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to Unknown variant to not repeat the same.

Comment thread core/src/model.rs
Comment on lines +257 to +259
InterchangeProjectUsageRaw::Unknown(unknown) => {
log::info!("{header}{removed:>12}{header:#} {unknown}");
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unreachable!() by definition; we don't know how to interpret this usage, so for sure can't remove it.

Comment on lines +81 to +83
// Never a standard library, which is all `excluded_iris`
// holds, and worth seeing: it is the one usage `info` can say
// nothing else about.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for this comment.

Comment thread core/src/solve/pubgrub.rs
Comment on lines +520 to +529
// `validate` refuses a usage this build cannot interpret, so one
// never reaches the solver. Selecting nothing is the safe answer
// if that ever changes: it rules this candidate out rather than
// resolving a dependency sysand does not understand.
InterchangeProjectUsage::Unknown(_) => {
deps.push((
DependencyIdentifier::Remote(usage.to_owned()),
DiscreteHashSet::empty(),
));
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This must panic instead of failing silently.

Comment thread core/src/resolve/mod.rs
Comment on lines +181 to +184
// Unreachable in practice -- `validate` refuses an
// uninterpretable usage long before one is resolved -- but there
// is a truthful thing to print, so print it.
InterchangeProjectUsage::Unknown(unknown) => write!(f, "{unknown}")?,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an important invariant that this is unreachable!(), so use it.

Comment on lines +37 to +44
// `validate` refuses a usage this build cannot interpret, so one
// never reaches a resolver. Reporting it as unsupported rather
// than resolving it is the safe answer if that ever changes.
InterchangeProjectUsage::Unknown(unknown) => {
Ok(ResolutionOutcome::UnsupportedUsageType {
reason: unknown.to_string(),
})
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unknown usage by definition will never be resolvable, so it must be marked unreachable!() in all places related to resolution.

Comment thread core/src/commands/add.rs
}
// `usage` passed `validate` above, which refuses an
// uninterpretable entry, so there is no merge path to take here.
InterchangeProjectUsageRaw::Unknown(_) => {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unreachable!(); all such places must panic.

}

#[test]
fn add_leaves_a_usage_it_cannot_interpret_alone() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to fail in this case, as no merging/duplicate detection can be performed, so the manifest may become invalid.

Comment thread core/src/resolve/file.rs
Comment on lines +350 to +357
// `validate` refuses a usage this build cannot interpret, so one
// never reaches a resolver. Reporting it as unsupported rather
// than resolving it is the safe answer if that ever changes.
InterchangeProjectUsage::Unknown(unknown) => {
Ok(ResolutionOutcome::UnsupportedUsageType {
reason: unknown.to_string(),
})
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unreachable!()

.usage
.iter()
.map(|u| Identifier::from_interchange_usage_unchecked(u).into_string())
.filter_map(|u| Identifier::from_unvalidated_usage(u).map(Identifier::into_string))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something already went wrong if this is reached, so this must panic on Unknown.

.usage
.into_iter()
.map(|u| Identifier::from_interchange_usage_unchecked(&u).into_string())
.filter_map(|u| Identifier::from_unvalidated_usage(&u).map(Identifier::into_string))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must panic on Unknown.

Comment on lines +73 to +74
| InterchangeProjectUsage::KparPath { .. }
| InterchangeProjectUsage::Unknown(_) => false,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unreachable!()

None,
InterchangeProjectUsage::Directory { .. }
| InterchangeProjectUsage::KparPath { .. }
| InterchangeProjectUsage::Unknown(_)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unreachable!()

@andrius-puksta-sensmetry andrius-puksta-sensmetry left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess supporting this cleanly would require introducing a new type, for which there is no time now. Current implementation is fine functionality-wise, but silently accepts incorrect behavior.

@andrius-puksta-sensmetry
andrius-puksta-sensmetry marked this pull request as draft September 24, 2026 08:17
@andrius-puksta-sensmetry

Copy link
Copy Markdown
Collaborator

We decided to go in the opposite direction with #566.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants