feat(core)!: add the typed index usage - #566
vytautas-astrauskas-sensmetry wants to merge 11 commits into
Conversation
Normalized values must not appear in typed usages, so the actual publisher/name has to be looked up in the index. This exposes a not-so-nice tradeoff: if we want to retain support for shorthand notation
|
Yes, the tradeoff is not nice.
Non-normalized values may contain spaces and, therefore, could be non-ergonomic.
This breaks with |
An index usage names a project by publisher and name, and has to spell them exactly as the project does. `add` now accepts two spellings: the project's own, which is checked, and the fully normalized one (lowercase, hyphens for spaces), which takes the project's spelling. - When locking, a normalized usage is first resolved by identifier as a placeholder, then written with the spelling of the project locked. Any other spelling is checked by the lock, as before. - With `--no-lock`, the spelling is checked against, or taken from, the versions installed in the local environment that match the version constraint, without any network request. It fails when none is installed, or when those installed disagree on the spelling. - A clash with a usage of another kind is reported before anything is looked up. The lock's spelling error now says outright that the usage resolved but was rejected for its spelling, and which spelling to use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An index usage has to spell a project's publisher and name exactly as the project does, so every version of a project in an index has to spell them the same way. `sysand index add` now refuses a version spelled differently from the versions already in the index (yanked ones included, removed ones left out), and any version of a project whose existing versions already disagree. The index protocol records one spelling per project as a server obligation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`add` now runs `sysand add` itself: after declaring the usage it locks and syncs, unless `no_lock` or `no_sync`, and restores `.project.json` if either fails. It takes `no_lock`, `no_sync`, `no_prune`, `resolution` and `auth`, and checks and recovers the spelling of index usages the way the CLI does. With `no_lock=True`, it only edits `.project.json`, and an index usage needs a matching version installed in the environment. `version_constraint` stays required. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
5547a90 to
e5cd21f
Compare
|
@andrius-puksta-sensmetry I haven't acted on discussion about name and publisher normalization yet in sysand.com. @vytautas-astrauskas-sensmetry there is a related issue for sysand.com about forcing sysand.com users to use the same name etc. With a sysand.com biased perspective, I'd like to avoid a change here, as for sysand.com there is no upside but a downside: users may want a change for some reason -- an initial typo or SENSmetry renaming to Sensmetry etc. @andrius-puksta-sensmetry confirm again and I'll follow through with sysand.com directly or in the future if you don't consider it high prio. |
I think it would make more sense for Python bindings to take |
Co-authored-by: Andrius Pukšta <andrius.puksta@sensmetry.com> Signed-off-by: vytautas-astrauskas-sensmetry <vytautas.astrauskas@sensmetry.com>
Co-authored-by: Andrius Pukšta <andrius.puksta@sensmetry.com> Signed-off-by: vytautas-astrauskas-sensmetry <vytautas.astrauskas@sensmetry.com>
- Python `add` takes `lock`, `sync` and `prune` (all `True` by default) instead of `no_lock`, `no_sync` and `no_prune`, avoiding double negation. - Removing by publisher and name, in Python and in `sysand remove <publisher>/<name>`, removes the project's usages of every kind: index, directory and KPAR usages, and a `pkg:sysand` resource usage. Each field matches spelled as the usage spells it, or normalized, as #574 does for the CLI. Python can now remove directory and KPAR usages, so the Python special cases for "cannot remove them yet" are gone. - Every typed usage kind (directory, KPAR, index) now rejects keys it does not define, not only index usages, so a future field such as a constraint on a directory usage is never silently dropped. Resource usages, the shape KerML specifies, keep ignoring unknown keys, since existing manifests carry extra keys in them. Index protocol §14 and RELEASE.md say so. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Changed in c38d6c2 |
Co-authored-by: Andrius Pukšta <andrius.puksta@sensmetry.com> Signed-off-by: vytautas-astrauskas-sensmetry <vytautas.astrauskas@sensmetry.com>
| fn of(usage: &'a InterchangeProjectUsage) -> Self { | ||
| match usage { | ||
| // Resource usages may produce invalid candidates that should not fail | ||
| // the whole resolution (e.g. both src and kpar variants for paths and http) | ||
| InterchangeProjectUsage::Resource { .. } => Self::Skip, | ||
| // Path usages name one project outright, so it must be a valid one | ||
| InterchangeProjectUsage::Directory { .. } | ||
| | InterchangeProjectUsage::KparPath { .. } => Self::Fail, | ||
| // A version an index offers is one its publisher published, so a | ||
| // broken one is a fault of the index to report, not to paper over | ||
| // by quietly picking another version | ||
| InterchangeProjectUsage::Index(IndexUsage { | ||
| version_constraint, .. | ||
| }) => Self::FailIfSelectable(version_constraint), | ||
| } | ||
| } |
There was a problem hiding this comment.
| fn of(usage: &'a InterchangeProjectUsage) -> Self { | |
| match usage { | |
| // Resource usages may produce invalid candidates that should not fail | |
| // the whole resolution (e.g. both src and kpar variants for paths and http) | |
| InterchangeProjectUsage::Resource { .. } => Self::Skip, | |
| // Path usages name one project outright, so it must be a valid one | |
| InterchangeProjectUsage::Directory { .. } | |
| | InterchangeProjectUsage::KparPath { .. } => Self::Fail, | |
| // A version an index offers is one its publisher published, so a | |
| // broken one is a fault of the index to report, not to paper over | |
| // by quietly picking another version | |
| InterchangeProjectUsage::Index(IndexUsage { | |
| version_constraint, .. | |
| }) => Self::FailIfSelectable(version_constraint), | |
| } | |
| } | |
| fn of(usage: &'a InterchangeProjectUsage) -> Self { | |
| // Candidates from env don't change the logic here | |
| match usage { | |
| // Resource usages may produce invalid candidates that should not fail | |
| // the whole resolution (e.g. both src and kpar variants for paths and http) | |
| InterchangeProjectUsage::Resource { .. } => Self::Skip, | |
| // Path usages name one project outright, so it must be a valid one | |
| InterchangeProjectUsage::Directory { .. } | |
| | InterchangeProjectUsage::KparPath { .. } => Self::Fail, | |
| // A version an index offers is one its publisher published, so a | |
| // broken one is a fault of the index to report, not to paper over | |
| // by quietly picking another version | |
| InterchangeProjectUsage::Index(IndexUsage { | |
| version_constraint, .. | |
| }) => Self::FailIfSelectable(version_constraint), | |
| } | |
| } |
| fn describe(&self) -> String { | ||
| match self { | ||
| Self::Resolved(e) => format!("is error: {}", format_err(e)), | ||
| Self::InvalidVersion { version, source } => { | ||
| format!("has invalid version `{version}`: {}", format_err(source)) | ||
| } | ||
| Self::MissingVersion => "did not expose a version".to_owned(), | ||
| Self::VersionObtain(e) => format!("failed to get version: {}", format_err(e)), | ||
| Self::InvalidProject { version, source } => { | ||
| format!("{version} has invalid usage: {}", format_err(source)) | ||
| } | ||
| Self::MissingUsage => "did not expose usages".to_owned(), | ||
| Self::UsageObtain(e) => format!("failed to get usages: {}", format_err(e)), | ||
| } | ||
| } |
There was a problem hiding this comment.
Self already implements Display; not worth duplicating for minor wording differences.
| /// Displayed as what is wrong with the candidate, for | ||
| /// [`InternalSolverError::BrokenIndexVersion`] to report. | ||
| #[derive(Error, Debug)] | ||
| pub enum CandidateFault<R: ResolveRead> { |
There was a problem hiding this comment.
| pub enum CandidateFault<R: ResolveRead> { | |
| pub enum CandidateError<R: ResolveRead> { |
Consistency
| match read_candidate::<R>(alternative) { | ||
| Ok(candidate) => found.push(candidate), | ||
| Err(fault) if on_broken.may_skip(&fault) => { | ||
| log::debug!("candidate project for {resolve} {}", fault.describe()); |
There was a problem hiding this comment.
| log::debug!("candidate project for {resolve} {}", fault.describe()); | |
| log::debug!("skipping candidate project for {resolve}: {}", format_err(fault)); |
| /// The project `publisher`/`name` from the configured indexes, or from | ||
| /// any other source that resolves by identity (the local environment, | ||
| /// `[[project]]` overrides, workspace members). | ||
| Index(IndexUsage<VersionReq>), | ||
| } | ||
|
|
||
| /// An index usage (see [`InterchangeProjectUsageG::Index`]). `publisher` and | ||
| /// `name` must match the resolved project's, without any normalization. | ||
| // `deny_unknown_fields`: see `Usage` | ||
| #[derive(Eq, Clone, PartialEq, Serialize, Deserialize, Hash, Debug)] | ||
| #[cfg_attr( | ||
| feature = "python", | ||
| derive(FromPyObject, IntoPyObject), | ||
| pyo3(from_item_all) | ||
| )] | ||
| #[serde(rename_all = "camelCase", deny_unknown_fields)] | ||
| pub struct IndexUsage<VersionReq> { | ||
| pub publisher: String, | ||
| pub name: String, | ||
| pub version_constraint: VersionReq, | ||
| } |
There was a problem hiding this comment.
Why is index usage a struct, but KparPath/Directory are not? Also, statement about publisher/name exact matching applies to all types usages.
| // Says all there is to say about which usage failed, and why | ||
| DependencyIdentifier::Requested(_) | ||
| if matches!(source, InternalSolverError::BrokenIndexVersion { .. }) => | ||
| { | ||
| write!(f, "{source}") | ||
| } | ||
| DependencyIdentifier::Requested(_) => { | ||
| write!(f, "failed to retrieve project(s): {source}") | ||
| } | ||
| DependencyIdentifier::Remote(iri) => { | ||
| write!(f, "failed to retrieve usages of `{iri}`: {source}") | ||
| } |
There was a problem hiding this comment.
Why special case for ::Requested, but not for ::Remote?
| impl<R: ResolveRead + fmt::Debug + 'static> std::error::Error for SolverError<R> { | ||
| /// `Display` already includes the solver's own error, so this skips to | ||
| /// what caused it | ||
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | ||
| match self.inner.as_ref() { | ||
| pubgrub::PubGrubError::ErrorRetrievingDependencies { source, .. } | ||
| | pubgrub::PubGrubError::ErrorChoosingVersion { source, .. } => { | ||
| std::error::Error::source(source) | ||
| } | ||
| pubgrub::PubGrubError::NoSolution(_) | ||
| | pubgrub::PubGrubError::ErrorInShouldCancel(_) => None, | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
| impl<R: ResolveRead + fmt::Debug + 'static> std::error::Error for SolverError<R> { | |
| /// `Display` already includes the solver's own error, so this skips to | |
| /// what caused it | |
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
| match self.inner.as_ref() { | |
| pubgrub::PubGrubError::ErrorRetrievingDependencies { source, .. } | |
| | pubgrub::PubGrubError::ErrorChoosingVersion { source, .. } => { | |
| std::error::Error::source(source) | |
| } | |
| pubgrub::PubGrubError::NoSolution(_) | |
| | pubgrub::PubGrubError::ErrorInShouldCancel(_) => None, | |
| } | |
| } | |
| } | |
| impl<R: ResolveRead + fmt::Debug + 'static> std::error::Error for SolverError<R> { | |
| fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { | |
| Some(self.inner.as_ref()) | |
| } | |
| } |
And change Display to not print error source.
| /// A version offered for an index usage is not a valid project. It is | ||
| /// not skipped for another version, which would make what is locked | ||
| /// depend on which versions happen to be broken |
There was a problem hiding this comment.
| /// A version offered for an index usage is not a valid project. It is | |
| /// not skipped for another version, which would make what is locked | |
| /// depend on which versions happen to be broken | |
| /// A version offered for an index usage is not a valid project. It is | |
| /// not skipped for another version, which would make the choice of | |
| /// projects depend on which versions happen to be broken |
| /// An index usage of the same project is already declared, but spelled | ||
| /// differently. Only one of the spellings can match the project's own. | ||
| #[error( | ||
| "`{new}` is already declared as the index usage `{existing}`;\n\ | ||
| an index usage must spell the publisher and name exactly as the project does" | ||
| )] | ||
| IndexUsageSpelledDifferently { existing: String, new: String }, |
There was a problem hiding this comment.
Applies to all typed usages.
| /// Whether `publisher` and `name` are both in normalized form, that is, | ||
| /// what [`normalize_field`] makes of them. An index usage given this way names | ||
| /// the project by its identifier only, and its actual spelling has to be | ||
| /// recovered (see [`spell_index_usage`]); any other spelling has to be the | ||
| /// project's own. | ||
| pub fn is_normalized_spelling(publisher: &str, name: &str) -> bool { | ||
| normalize_field(publisher) == publisher && normalize_field(name) == name | ||
| } |
There was a problem hiding this comment.
| /// Whether `publisher` and `name` are both in normalized form, that is, | |
| /// what [`normalize_field`] makes of them. An index usage given this way names | |
| /// the project by its identifier only, and its actual spelling has to be | |
| /// recovered (see [`spell_index_usage`]); any other spelling has to be the | |
| /// project's own. | |
| pub fn is_normalized_spelling(publisher: &str, name: &str) -> bool { | |
| normalize_field(publisher) == publisher && normalize_field(name) == name | |
| } | |
| /// Whether `publisher` and `name` are both in normalized form, that is, | |
| /// what [`normalize_field`] makes of them. A typed usage given this way names | |
| /// the project by its identifier only, and its actual spelling has to be | |
| /// recovered (see [`spell_index_usage`]); any other spelling has to be the | |
| /// project's own. | |
| pub fn is_normalized_spelling(publisher: &str, name: &str) -> bool { | |
| normalize_field(publisher) == publisher && normalize_field(name) == name | |
| } |
There was a problem hiding this comment.
For non-index typed usages are given by their source only, but specifying. publisher+name for them will be allowed in the future.
| Some(InterchangeProjectUsageRaw::Resource { .. }) | None => { | ||
| Err(RemoveError::ExpUsageNotFound { | ||
| publisher: publisher.to_owned(), | ||
| name: name.to_owned(), | ||
| }) | ||
| } |
There was a problem hiding this comment.
| Some(InterchangeProjectUsageRaw::Resource { .. }) | None => { | |
| Err(RemoveError::ExpUsageNotFound { | |
| publisher: publisher.to_owned(), | |
| name: name.to_owned(), | |
| }) | |
| } | |
| Some(InterchangeProjectUsageRaw::Resource { .. }) | None => { | |
| unreachable!() | |
| } |
| ) | ||
| resolved = project_iri("add", iri, publisher, name) | ||
| return sysand_rs.do_add_py(str(project_dir), resolved, version_constraint) # type: ignore | ||
| check_named("add", iri, publisher, name) |
There was a problem hiding this comment.
This now duplicates the check in do_add_py. Since all the arguments are passed to it anyway, remove the check here.
| pub struct IndexUsageMismatchError { | ||
| pub usage_publisher: String, | ||
| pub usage_name: String, | ||
| pub declared_by: DeclaredBy, | ||
| pub version: String, | ||
| pub publisher: Option<String>, | ||
| pub name: String, | ||
| } |
There was a problem hiding this comment.
Generalize to all typed usages.
| let mut index_usages: Vec<(IndexUsage<VersionReq>, DeclaredBy)> = inputs | ||
| .iter() | ||
| .zip(declared_by) | ||
| .filter_map(|(usage, declared_by)| match usage { | ||
| InterchangeProjectUsage::Index(index) => Some((index.clone(), declared_by)), | ||
| _ => None, | ||
| }) | ||
| .collect(); |
There was a problem hiding this comment.
This applies to all typed usages.
There was a problem hiding this comment.
Either unify checks from src/kpar readers here, or move this check to index resolver.
| dependencies.push((identifier, project)); | ||
| } | ||
|
|
||
| check_index_usages(index_usages, &solved).map_err(LockError::IndexUsageMismatch)?; |
There was a problem hiding this comment.
What about transitive usages? This requirement applies to them equally.
andrius-puksta-sensmetry
left a comment
There was a problem hiding this comment.
Didn't pay much attention to CLI crate due to the looming rebase changes.
Adds a fourth usage kind, the index usage:
{ "publisher": "Acme Labs", "name": "My Lib", "versionConstraint": "^1.2" }It names a project by its publisher and name, spelled as that project spells them, and resolves through everything that resolves by identity: the configured indexes,
.sysand,[[project]]overrides and workspace members. Resolution itself goes by the normalized identifier (pkg:sysand/acme-labs/my-lib); the exact spelling is checked on top. It replaces the Python-only index view, which showed apkg:sysandresource usage as an index usage and lost the spelling.What changes for users
sysand add <publisher>/<name>writes an index usage (sysand add "Acme Labs/My Lib" ^1). It used to write apkg:sysandresource usage. A fullpkg:sysand/...IRI still writes a resource usage.addtakes two spellings: the project's own, which it checks, or the fully normalized one (acme-labs/my-lib), from which it takes the project's own spelling. Any other spelling fails.--no-lock, the spelling is checked against, or taken from, the versions installed in.sysandthat match the constraint, with no network request.addfails when none is installed, or when the installed ones disagree on the spelling, with a hint to leave out--no-lock.addwrites^the version that locking chose, ascargo adddoes. With--no-lock, a constraint must be given.syncfrom an existing lockfile does not re-solve, so it skips this check.sysand remove <publisher>/<name>removes the index usage spelled exactly so. When a usage of the same project exists but doesn't match, the error says what is there instead: another spelling ("did you mean"), a legacy PURL, or another kind, with the command that removes it.pkg:sysandresource usages stay as they are. Adding an index usage over one is refused, with a hint to remove the legacy usage first.--strict-index-versions: by default, the solver skips a version offered for an index usage when that version's own metadata is invalid, and picks among the others. With the flag, such a version fails the solve. It applies to index usages only.sysand index addkeeps one spelling per project. It refuses a version spelled differently from the versions already in the index (yanked ones included, removed ones left out). It also refuses any version of a project whose existing versions already disagree.addlocks and syncs, assysand adddoes, unlessno_lock=Trueorno_sync=True, and restores.project.jsonif either fails. It takesno_lock,no_sync,no_prune,resolutionandauth, and checks and recovers spellings the same way as the CLI. This breaks callers that relied onaddonly editing the manifest: they now passno_lock=True.version_constraintstays required;remove/set_usage_constraint(publisher=, name=)work on index usages by exact spelling;iri=is always a resource usage, and apkg:sysandIRI reads back asInterchangeProjectUsageResource;InterchangeProjectUsageIndex.version_constraintis required;Resolution(strict_index_versions=True)affectslock.InterchangeProjectUsageIndexclass.Compatibility
versions.json.buildkeeps the usage typed in the KPAR. Index protocol §11 now also requires an index to serve one spelling per project and refuse a version spelled differently, which sysand.com already does.parse a manifest that contains an index usage. The same goes for the
versions.jsonof any index project that has one in any version. Publishedusagenever changes (protocol §11), so older clients lose every version ofsuch a project for good.
publisher,nameandversionConstraintis rejected rather than ignored, so that afuture kind, a typo, or an index-selecting key is never read as a plain
index usage.
RELEASE.mdand index protocol §14 record the exception.Follow-ups
sysand index addand the sysand.com server should refuse a versionwhose index usages misspell a project the index already has. Until then,
a spelling mismatch in a published dependency fails every downstream
lock, and no downstream user can fix it.
sysand.tomlsetting for strict index versions.