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
39 changes: 39 additions & 0 deletions src/packages/agent-runtime/src/__tests__/structural-review.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions src/packages/agent-runtime/src/application/context-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
8 changes: 1 addition & 7 deletions src/rulesets/adr/adr-0002-hexagonal-architecture.rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add native HXA-03 coverage before dropping enforcement

When ADR-0002 is evaluated through the Native/Composite engine, HXA-03 now has no enforce block, so it is routed to NativeEvaluator; however, I checked ArchitectureRuleHandler/STRUCTURAL_CATEGORIES and layer-structure is not handled, and a repo-wide search shows no native HXA-03 implementation. Skipped native results are filtered out of validation issues, so a satellite whose infrastructure does not implement Core ports can pass without any blocking finding. Please add the native HXA-03 logic (or keep an actually evaluated fallback path) before removing this enforcement block.

Useful? React with 👍 / 👎.

},
{
"id": "HXA-04",
Expand Down