Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"dspack": "0.3",
"name": "negative-fixture",
"components": {
"button": {
"name": "Button",
"description": "A button.",
"props": {
"variant": {
"type": "enum",
"values": [
"default",
"destructive"
]
}
}
},
"alert-dialog": {
"name": "AlertDialog",
"description": "A confirmation dialog.",
"composition": {
"subComponents": [
{
"id": "alert-dialog-title",
"name": "AlertDialogTitle"
},
{
"id": "alert-dialog-cancel",
"name": "AlertDialogCancel"
}
]
}
}
},
"intents": [
{
"id": "destructive-action",
"description": "Irreversible operations."
}
],
"_comment": "CONSISTENCY: requiredSubComponents references 'ghost-panel', which is declared neither as a top-level component nor as a composition sub-component. (Ids declared as EITHER kind resolve — descendants match by node component id; see rule.review-offers-alternatives in examples/astryx.dspack.json for the top-level-component case and rule.alertdialog-requires-cancel in examples/shadcn-ui.dspack.json for the sub-component case.)",
"rules": [
{
"id": "rule.fixture",
"severity": "must",
"rationale": "Fixture rationale.",
"type": "required-composition",
"appliesTo": {
"intents": [
"destructive-action"
]
},
"component": "alert-dialog",
"requiredSubComponents": [
{
"id": "ghost-panel",
"min": 1
}
]
}
]
}
10 changes: 8 additions & 2 deletions scripts/validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -243,12 +243,18 @@ function checkGovernance(doc, validateSurface) {
for (const { kind, id } of ruleComponentRefs(rule)) {
const resolvesToComponent = vocab.components.has(id);
const resolvesToSub = vocab.subComponents.has(id);
// `requiredSubComponents` entries match descendant NODES by component id
// at lint time (spec §5), so the id may be declared as a top-level
// component or as a composition sub-component. Resolution here only
// guards the vocabulary; satisfaction (matching descendants beneath each
// governed node) is the S3 gate's concern, not this harness's.
// `on` remains sub-component-only (spec §5: "the sub-component id `on`").
const ok =
kind === "requiredSubComponents" || kind === "on"
kind === "on"
? resolvesToSub
: kind === "component"
? resolvesToComponent
: resolvesToComponent || resolvesToSub; // componentOrSub, require, forbid, forbiddenDescendants
: resolvesToComponent || resolvesToSub; // requiredSubComponents, componentOrSub, require, forbid, forbiddenDescendants
if (!ok) errors.push(`${rule.id}: ${kind} reference '${id}' does not resolve in the contract`);
}
for (const ex of rule.examples ?? []) {
Expand Down
5 changes: 5 additions & 0 deletions spec/dspack-v0.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,11 @@ present and equal to a member of `oneOf`; when `on` is given, **every** descenda
the sub-component id `on` MUST satisfy the prop constraint, and at least one such descendant
MUST exist.

A `requiredSubComponents` entry's `id` MUST be declared in the contract — either as a
top-level component or as a composition sub-component; descendants match by node component
id in both cases. Declaration alone never satisfies the entry: satisfaction requires the
matching descendants described above, beneath each governed node.

**`forbidden-composition`** — structure and values no instance of a component may contain.
Fields: `component: string`, `forbiddenDescendants?: string[]`,
`forbiddenProps?: {on?, prop, values}[]` (at least one of the two present).
Expand Down
Loading