Skip to content

Commit b8ced66

Browse files
author
Dean Sharon
committed
feat: add DDL duplication, file size, placeholder, and circular dep checkers
DDL duplication checker (structural warning) detects CREATE TABLE statements duplicated from canonical schema files. File size checker (structural warning) enforces per-type line limits from FRAMEWORK.md. Placeholder marker checker (semantic advisory) flags TBD/TODO/FIXME outside code fences. Circular dependency detection added to dependency health checker. Extract shared getNonProseLines utility. Update templates with framework guardrails. Update Feature Composer agent with rules. Add schema purity check to Semantic Validator agent.
1 parent e2dc9d1 commit b8ced66

18 files changed

Lines changed: 920 additions & 75 deletions

assets/templates/decision.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ Description of alternative and why it was not chosen.
4747

4848
Description of alternative and why it was not chosen.
4949

50+
## Implementation Notes
51+
52+
> Do not duplicate DDL or implementation SQL here. Link to canonical schema files instead.
53+
5054
## References
5155

5256
- Related documents or external references

assets/templates/schema.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ description: {{description}}
99

1010
{{description}}
1111

12+
> **Content guidelines:** This file should contain DDL, constraints, indexes, RLS, and reference data only. Workflows, state machines, behavioral descriptions, and domain logic belong in domain files.
13+
1214
## Tables
1315

1416
### {{table_name}}
1517

1618
| Column | Type | Constraints | Description |
1719
|--------|------|-------------|-------------|
1820
| id | uuid | PK | Primary identifier |
19-
| tenant_id | uuid | FK, NOT NULL | Tenant reference |
21+
| organization_id | uuid | FK, NOT NULL | Organization reference |
2022
| created_at | timestamp | NOT NULL | Creation timestamp |
2123
| updated_at | timestamp | NOT NULL | Last update timestamp |
2224

@@ -29,11 +31,11 @@ description: {{description}}
2931
| Name | Columns | Type | Purpose |
3032
|------|---------|------|---------|
3133
| {{table_name}}_pkey | id | PRIMARY | Primary key |
32-
| {{table_name}}_tenant_id_idx | tenant_id | BTREE | Tenant lookup |
34+
| {{table_name}}_organization_id_idx | organization_id | BTREE | Organization lookup |
3335

3436
## Constraints
3537

36-
- `tenant_id` must reference a valid tenant
38+
- `organization_id` must reference a valid organization
3739
- `created_at` cannot be modified after creation
3840

3941
## Usage
@@ -45,4 +47,4 @@ import { {{name}} } from 'your-org/database';
4547

4648
## Related Schemas
4749

48-
- [related-schema](./related-schema.md) - Tenant ownership
50+
- [related-schema](./related-schema.md) - Organization ownership

plugins/spec-core/agents/feature-composer.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,3 +268,13 @@ Before creating any file, validate:
268268
3. **Path Valid** - Target path matches component type
269269
4. **No Duplicates** - File doesn't already exist
270270
5. **Layer Rules** - Any links in template are valid for layer
271+
272+
## Framework Rules
273+
274+
Generated content must comply with these rules from FRAMEWORK.md:
275+
276+
- **Rule 1 (No Duplication):** Never duplicate content. Always link to the canonical source. If information exists elsewhere, reference it with a markdown link.
277+
- **Rule 3 (File Size Limits):** schema: 400 lines, pattern: 300 lines, domain-topic: 500 lines, feature: 400 lines. All other types: 500 lines max.
278+
- **Rule 7 (No DDL in ADRs):** Decision records must not contain CREATE TABLE or other DDL. Link to canonical schema files instead.
279+
- **Rule 8 (Schema Purity):** Schema files contain structure only — DDL, constraints, indexes, RLS, and reference data. Workflows, state machines, behavioral descriptions, and domain logic belong in domain files.
280+
- **Rule 9 (No Placeholders):** Do not leave TBD, TODO, FIXME, HACK, or PLACEHOLDER markers in generated content. Use honest stubs with "Under Review" banners if content is not yet decided.

plugins/spec-core/agents/semantic-validator.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,33 @@ Concepts or entities mentioned without links to their defining documents.
9292
- Terms defined in the same file
9393
- Links that exist but point to a different section
9494

95+
#### d. Schema Purity (for files matching `docs/schemas/*.md`)
96+
97+
Check that content stays within structural scope.
98+
99+
**What counts as a finding:**
100+
- Workflow descriptions ("when X happens, then Y")
101+
- State machine transitions ("pending -> resolved -> dismissed")
102+
- Business rules ("only admins can...")
103+
- Process flows with multiple steps
104+
- Behavioral descriptions explaining system reactions to events
105+
106+
**What does NOT count:**
107+
- DDL (CREATE TABLE, indexes, constraints, RLS policies)
108+
- Enum value catalogs (list of valid event types, status values)
109+
- Relationship diagrams (showing foreign key connections)
110+
- Constraint explanations ("must be unique because...")
111+
- Column descriptions (what each field stores)
112+
- "Referenced By" sections
113+
114+
Report format:
115+
```
116+
SCHEMA_PURITY
117+
File: docs/schemas/events.md
118+
Finding: Contains "Alert Status Flow" section describing state transitions
119+
Suggestion: Move to appropriate domain file.
120+
```
121+
95122
### Step 4: Cross-Domain Analysis
96123

97124
After all groups are analyzed individually, check across domain boundaries:

src/validation/semantic/config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export interface SemanticConfig {
3636
domainCoupling: {
3737
domains: Record<string, string[]>; // domain name → marker terms
3838
};
39+
placeholders?: {
40+
enabled: boolean;
41+
markers: string[];
42+
};
3943
ignore: string[]; // glob patterns to skip
4044
}
4145

@@ -191,6 +195,22 @@ function validateConfigShape(raw: unknown): string | null {
191195
}
192196
}
193197

198+
// placeholders
199+
if (raw['placeholders'] !== undefined) {
200+
if (!isRecord(raw['placeholders'])) {
201+
return '"placeholders" must be an object';
202+
}
203+
const ph = raw['placeholders'];
204+
if (ph['enabled'] !== undefined && typeof ph['enabled'] !== 'boolean') {
205+
return '"placeholders.enabled" must be a boolean';
206+
}
207+
if (ph['markers'] !== undefined) {
208+
if (!Array.isArray(ph['markers']) || !ph['markers'].every((v): v is string => typeof v === 'string')) {
209+
return '"placeholders.markers" must be an array of strings';
210+
}
211+
}
212+
}
213+
194214
// ignore
195215
if (raw['ignore'] !== undefined) {
196216
if (!Array.isArray(raw['ignore']) || !raw['ignore'].every((v): v is string => typeof v === 'string')) {
@@ -213,6 +233,17 @@ function mergeWithDefaults(raw: Record<string, unknown>): SemanticConfig {
213233
const cr = isRecord(raw['crossReference']) ? raw['crossReference'] : {};
214234
const dh = isRecord(raw['dependencyHealth']) ? raw['dependencyHealth'] : {};
215235
const dc = isRecord(raw['domainCoupling']) ? raw['domainCoupling'] : {};
236+
const ph = isRecord(raw['placeholders']) ? raw['placeholders'] : null;
237+
238+
// Build placeholders config only if present in raw config
239+
const placeholdersConfig: { placeholders: { enabled: boolean; markers: string[] } } | Record<string, never> = ph !== null
240+
? {
241+
placeholders: {
242+
enabled: typeof ph['enabled'] === 'boolean' ? ph['enabled'] : false,
243+
markers: Array.isArray(ph['markers']) ? (ph['markers'] as string[]) : [],
244+
},
245+
}
246+
: {};
216247

217248
return {
218249
terminology: {
@@ -265,6 +296,7 @@ function mergeWithDefaults(raw: Record<string, unknown>): SemanticConfig {
265296
? (dc['domains'] as Record<string, string[]>)
266297
: defaults.domainCoupling.domains,
267298
},
299+
...placeholdersConfig,
268300
ignore: Array.isArray(raw['ignore']) ? (raw['ignore'] as string[]) : defaults.ignore,
269301
};
270302
}

src/validation/semantic/dependency-health.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,84 @@ describe('checkDependencyHealth', () => {
143143
const issues = checkDependencyHealth(files, config);
144144
expect(issues).toHaveLength(0);
145145
});
146+
147+
it('detects A → B → A circular dependency', () => {
148+
const files = new Map([
149+
[
150+
'docs/domains/auth/overview.md',
151+
'---\ntitle: Auth\ndependencies: users\n---\n# Auth',
152+
],
153+
[
154+
'docs/domains/users/overview.md',
155+
'---\ntitle: Users\ndependencies: auth\n---\n# Users',
156+
],
157+
]);
158+
const config = makeConfig();
159+
160+
const issues = checkDependencyHealth(files, config);
161+
const circularIssues = issues.filter((i) => i.code === 'CIRCULAR_DEPENDENCY');
162+
expect(circularIssues).toHaveLength(1);
163+
expect(circularIssues[0]!.message).toContain('auth');
164+
expect(circularIssues[0]!.message).toContain('users');
165+
});
166+
167+
it('detects A → B → C → A circular dependency', () => {
168+
const files = new Map([
169+
[
170+
'docs/domains/auth/overview.md',
171+
'---\ntitle: Auth\ndependencies: users\n---\n# Auth',
172+
],
173+
[
174+
'docs/domains/users/overview.md',
175+
'---\ntitle: Users\ndependencies: billing\n---\n# Users',
176+
],
177+
[
178+
'docs/domains/billing/overview.md',
179+
'---\ntitle: Billing\ndependencies: auth\n---\n# Billing',
180+
],
181+
]);
182+
const config = makeConfig();
183+
184+
const issues = checkDependencyHealth(files, config);
185+
const circularIssues = issues.filter((i) => i.code === 'CIRCULAR_DEPENDENCY');
186+
expect(circularIssues).toHaveLength(1);
187+
expect(circularIssues[0]!.message).toContain('Circular dependency detected');
188+
});
189+
190+
it('does not report circular dependency for acyclic chains', () => {
191+
const files = new Map([
192+
[
193+
'docs/domains/auth/overview.md',
194+
'---\ntitle: Auth\ndependencies: users\n---\n# Auth',
195+
],
196+
[
197+
'docs/domains/users/overview.md',
198+
'---\ntitle: Users\ndependencies: billing\n---\n# Users',
199+
],
200+
[
201+
'docs/domains/billing/overview.md',
202+
'---\ntitle: Billing\n---\n# Billing',
203+
],
204+
]);
205+
const config = makeConfig();
206+
207+
const issues = checkDependencyHealth(files, config);
208+
const circularIssues = issues.filter((i) => i.code === 'CIRCULAR_DEPENDENCY');
209+
expect(circularIssues).toHaveLength(0);
210+
});
211+
212+
it('detects self-dependency', () => {
213+
const files = new Map([
214+
[
215+
'docs/domains/auth/overview.md',
216+
'---\ntitle: Auth\ndependencies: auth\n---\n# Auth',
217+
],
218+
]);
219+
const config = makeConfig();
220+
221+
const issues = checkDependencyHealth(files, config);
222+
const circularIssues = issues.filter((i) => i.code === 'CIRCULAR_DEPENDENCY');
223+
expect(circularIssues).toHaveLength(1);
224+
expect(circularIssues[0]!.message).toContain('auth');
225+
});
146226
});

0 commit comments

Comments
 (0)