diff --git a/src/packages/agent-runtime/src/__tests__/structural-review.spec.ts b/src/packages/agent-runtime/src/__tests__/structural-review.spec.ts index af50dbb2..4d360b10 100644 --- a/src/packages/agent-runtime/src/__tests__/structural-review.spec.ts +++ b/src/packages/agent-runtime/src/__tests__/structural-review.spec.ts @@ -32,6 +32,10 @@ import { compareSeverity, } from '../domain/rubrics/structural-review-rubric'; import type { CollectionContext, CollectionTarget } from '../domain/ports/quality-signal-provider.port'; +import { DEFAULT_SKILLS } from '../adapters/skills/default-skills'; +import { buildEvaluationContext } from '../application/context-mapper'; +import { parseAgentRuntimeRequest } from '../domain/contracts/agent-runtime-request'; +import type { SkillDescriptor } from '../domain/contracts/capability'; /** A deterministic stub reviewer standing in for the probabilistic LLM/agent. */ function stubReviewer(findings: readonly RawStructuralFinding[]): IStructuralReviewer { @@ -208,3 +212,38 @@ describe('Structural Quality Gate (deterministic severity → decision)', () => expect(decideForSeverity('medium', DEFAULT_STRUCTURAL_GATE_POLICY)).toBe('warn'); }); }); + +/** + * Kind routing (GT-535) — the structural-review skill must forward a canonical + * EvaluationKind through buildEvaluationContext, NOT be dropped to the 'gate' + * fallback. Regression guard: 'code-quality' is a quality-signal DIMENSION, not a + * kind; the skill declares the canonical 'evidence' kind so the provider stays + * reachable when the IStructuralReviewer adapter lands. + */ +describe('structural-review skill kind routing (GT-535)', () => { + const skill = DEFAULT_SKILLS.find((s) => s.id === 'code-quality-structural-review'); + const req = parseAgentRuntimeRequest({ tenant: 't-1', intent: 'structural_review' }); + + it('declares a canonical evaluation kind (not the code-quality dimension)', () => { + expect(skill).toBeDefined(); + expect(skill?.evaluationKinds).toEqual(['evidence']); + // 'code-quality' is the Evidence DIMENSION, never an EvaluationKind. + expect(skill?.evaluationKinds).not.toContain('code-quality'); + }); + + it('forwards the declared kind through buildEvaluationContext (not the gate fallback)', () => { + const ctx = buildEvaluationContext(req, skill as SkillDescriptor); + expect(ctx.kinds).toEqual(['evidence']); + // Guard the regression: an unknown/dimension-shaped kind would collapse to ['gate']. + expect(ctx.kinds).not.toEqual(['gate']); + }); + + it('drops a non-canonical kind to the gate fallback (documents the filter)', () => { + const bogus: SkillDescriptor = { + ...(skill as SkillDescriptor), + evaluationKinds: ['code-quality'], + }; + const ctx = buildEvaluationContext(req, bogus); + expect(ctx.kinds).toEqual(['gate']); + }); +}); diff --git a/src/packages/agent-runtime/src/adapters/skills/default-skills.ts b/src/packages/agent-runtime/src/adapters/skills/default-skills.ts index 8f96bbc1..40c192f7 100644 --- a/src/packages/agent-runtime/src/adapters/skills/default-skills.ts +++ b/src/packages/agent-runtime/src/adapters/skills/default-skills.ts @@ -93,8 +93,14 @@ export const DEFAULT_SKILLS: readonly SkillDescriptor[] = [ intents: ['structural_review', 'code_quality_review', 'review_structure'], // Orchestration runs the probabilistic reviewer behind IStructuralReviewer and // hands the normalized Evidence to the Core as a code-quality signal (GT-535). + // NOTE: 'code-quality' is the quality-signal DIMENSION the emitted Evidence is + // tagged with (StructuralReviewProvider.DEFAULT_DIMENSION), NOT an EvaluationKind. + // The canonical EvaluationKind for declared quality-signal Evidence is 'evidence' + // (ADR-0111 / GT-533). Declaring it here keeps the kind routed by + // buildEvaluationContext instead of being dropped to the 'gate' fallback, so the + // StructuralReviewProvider stays reachable when the IStructuralReviewer adapter lands. kind: 'evaluation', - evaluationKinds: ['code-quality'], + evaluationKinds: ['evidence'], permissions: ['read:repo'], requiresApproval: false, emitsTrace: true, diff --git a/src/packages/agent-runtime/src/application/context-mapper.ts b/src/packages/agent-runtime/src/application/context-mapper.ts index e0da69dd..b8b55718 100644 --- a/src/packages/agent-runtime/src/application/context-mapper.ts +++ b/src/packages/agent-runtime/src/application/context-mapper.ts @@ -35,9 +35,18 @@ export function toPhaseId(phase?: string): PhaseIdT | undefined { return (CANONICAL_PHASES as readonly string[]).includes(v) ? (v as PhaseIdT) : undefined; } +/** + * Allowlist of canonical evaluation kinds a skill may forward to the Core. It MUST + * mirror the core-domain `EvaluationKind` union (evaluation-context.ts) — a skill + * that declares a kind absent from this list is silently dropped to the 'gate' + * fallback (that is the bug GT-535 hit: 'code-quality' is a quality-signal + * DIMENSION, not a kind). Kept as a literal (not imported) because this file is a + * type-only consumer of core-domain — the hexagon carries no runtime dependency. + */ const KNOWN_KINDS: readonly string[] = [ 'gate', 'artifact', 'evidence', 'architecture', 'blueprint', 'topology', 'checkpoint', 'deployment', 'rule', 'compliance', + 'design', 'phase-artifacts', ]; /** Build a canonical EvaluationContext from a runtime request + resolved skill. */ diff --git a/src/packages/core-domain/src/application/validators/enforcement/policy-compiler.spec.ts b/src/packages/core-domain/src/application/validators/enforcement/policy-compiler.spec.ts index 6952356a..25b0888b 100644 --- a/src/packages/core-domain/src/application/validators/enforcement/policy-compiler.spec.ts +++ b/src/packages/core-domain/src/application/validators/enforcement/policy-compiler.spec.ts @@ -170,22 +170,26 @@ describe('PolicyCompiler — compileRuleset partitions and never wholesale-fails }); describe('PolicyCompiler ⇄ ADR-0002 pilot (GT-516 AC1: HXA rules compile/fallback)', () => { - it('routes every HXA-01..07 rule through the enforcer and partitions compile vs per-rule fallback', () => { + it('routes the import-graph-expressible HXA rules through the enforcer and partitions compile vs per-rule fallback', () => { const ruleset = JSON.parse(readFileSync(findUp('src/rulesets/adr/adr-0002-hexagonal-architecture.rules.json'), 'utf8')); const rules: CompilableRule[] = ruleset.rules; const enforced = rules.filter((r) => r.enforce?.engine === 'enforcer'); - // The pilot populates an enforce block on all seven HXA rules. - expect(enforced.map((r) => r.id).sort()).toEqual(['HXA-01', 'HXA-02', 'HXA-03', 'HXA-04', 'HXA-05', 'HXA-06', 'HXA-07']); + // HXA-03 ("Infrastructure IMPLEMENTS Core ports") is a POSITIVE/structural assertion. + // dependency-cruiser is a forbidden-dependency tool and cannot express "X implements Y", + // so HXA-03 carries NO enforce block and is left on the native engine — routing it through + // the enforcer would only ever degrade to a native fallback (or, with a config, false-positive). + // The remaining six HXA rules are enforcer-routed. + expect(enforced.map((r) => r.id).sort()).toEqual(['HXA-01', 'HXA-02', 'HXA-04', 'HXA-05', 'HXA-06', 'HXA-07']); const { compiled, fallbacks } = compileRuleset(rules); // The five import-graph-expressible rules lower to a dependency-cruiser check. expect(compiled.map((c) => c.ruleId).sort()).toEqual(['HXA-01', 'HXA-02', 'HXA-04', 'HXA-05', 'HXA-07']); - // The two structural/positive rules (implements-ports, AOP-in-infra-only) are not - // import-graph-expressible → they take the documented per-rule NATIVE fallback rather - // than emitting an all-matching (false-positive-prone) rule, and never fail the run. - expect(fallbacks.map((f) => f.ruleId).sort()).toEqual(['HXA-03', 'HXA-06']); + // HXA-06 (AOP-in-infra-only) is enforcer-routed but not import-graph-expressible → it takes the + // documented per-rule NATIVE fallback rather than emitting an all-matching (false-positive-prone) + // rule, and never fails the run. (HXA-03 is native-only and never enters the enforcer partition.) + expect(fallbacks.map((f) => f.ruleId).sort()).toEqual(['HXA-06']); for (const f of fallbacks) { expect(f.fallback).toBe('native'); expect(f.tool).toBe('dependency-cruiser'); diff --git a/src/rulesets/adr/adr-0002-hexagonal-architecture.rules.json b/src/rulesets/adr/adr-0002-hexagonal-architecture.rules.json index b4d2692a..8ba371a1 100644 --- a/src/rulesets/adr/adr-0002-hexagonal-architecture.rules.json +++ b/src/rulesets/adr/adr-0002-hexagonal-architecture.rules.json @@ -63,13 +63,7 @@ "rationale": "ADR-0002 §Decision: Infrastructure is the adapter layer. It implements the ports defined by Core.", "validationQuery": "Infrastructure files implement interfaces defined in Core layer. No domain logic lives here.", "blocking": true, - "layer": "Infrastructure", - "enforce": { - "engine": "enforcer", - "tool": "dependency-cruiser", - "toolRuleId": "hxa-03-infra-implements-ports", - "runtime": "node" - } + "layer": "Infrastructure" }, { "id": "HXA-04",