feat: publish Dever core with project scaffolding and versioned AI skills

This commit is contained in:
Antigravity committed 2026-10-04 13:37:30 +00:00
1 parent 70fa73cdc2
commit ac458ffa7f
960 files changed
+221319 -1163

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
use std::collections::BTreeMap;
use crate::diagnostic::{Diagnostic, Label};
use crate::source::{SourceLayout, SourceMap, SourceRole, Span};
#[derive(Default)]
struct RoleFiles {
root: Option<Span>,
topics: Vec<Span>,
}
pub(super) fn validate(sources: &SourceMap) -> Vec<Diagnostic> {
let mut errors = Vec::new();
let mut roles: BTreeMap<(String, String, SourceRole), RoleFiles> = BTreeMap::new();
for source in sources.files() {
let layout = match source.layout() {
Ok(layout) => layout,
Err(error) => {
errors.push(error);
continue;
}
};
match layout {
SourceLayout::Main => {}
SourceLayout::Test {
component,
domain,
topic,
} => {
check_bucket_names(
[&component, &domain, &topic],
source.span(0, 0),
&mut errors,
);
}
SourceLayout::Loose if sources.strict_layout() => errors.push(Diagnostic::error(
"C003",
"application source must be main.dever or <component>/<domain>/<app|domain|model|port|adapter|api>.dever, with an optional role topic directory",
source.span(0, 0),
)),
SourceLayout::Loose => {}
SourceLayout::Role {
component,
domain,
role,
topics,
} => {
check_bucket_names(
std::iter::once(&component)
.chain(std::iter::once(&domain))
.chain(topics.iter()),
source.span(0, 0),
&mut errors,
);
if role != SourceRole::Api && topics.len() > 1 {
errors.push(Diagnostic::error(
"C003",
format!(
"{} topic directories are flat; nested directories are supported only below api/",
role.name()
),
source.span(0, 0),
));
}
let files = roles.entry((component, domain, role)).or_default();
if topics.is_empty() {
files.root = Some(source.span(0, 0));
} else {
files.topics.push(source.span(0, 0));
}
}
}
}
for ((_, _, role), files) in roles {
// Model topics name persisted entities; API topics name route paths.
// Folding either into the root file would change its public identity.
if matches!(role, SourceRole::Model | SourceRole::Api) {
continue;
}
if let (Some(root), Some(topic)) = (files.root, files.topics.first().copied()) {
let mut error = Diagnostic::error(
"C003",
format!(
"{}.dever and its {}/ directory are mutually exclusive",
role.name(),
role.name()
),
topic,
);
error.related.push(Label {
span: root,
message: "role file declared here".into(),
});
errors.push(error);
} else if files.root.is_none() && files.topics.len() == 1 {
errors.push(Diagnostic::error(
"C003",
format!(
"a {}/ directory needs at least two cohesive topic files; merge a single topic into {}.dever",
role.name(),
role.name()
),
files.topics[0],
));
}
}
errors
}
fn check_bucket_names<'a>(
names: impl IntoIterator<Item = &'a String>,
span: Span,
errors: &mut Vec<Diagnostic>,
) {
for name in names {
if matches!(
name.as_str(),
"common" | "shared" | "utils" | "helper" | "helpers" | "base" | "service" | "services"
) {
errors.push(Diagnostic::error(
"C003",
format!(
"'{name}' is a generic source bucket; name the business domain or cohesive topic instead"
),
span,
));
}
}
}