fix(ms-ai-architect): RX-P2 reg-kjede-wiring + komprehensiv agent-sti-forankring [skip-docs]

- classify->dpia->ros kjede-wiring: dpia/ros Task-templater far eksplisitte
  AI Act-klassifiserings- + DPIA-funn-felt (kjede-data agentene allerede branchet pa)
- dpia-agent uutforbar "spor bruker" -> "marker vurderingen" (Hvis-ikke-klassifisert
  + Error Handling); speilet til ros-analysis-agent
- adr-writer-agent: Write-verktoy fjernet, returnerer ADR-markdown til hovedkontekst
  (command er eneste skriver -- subagenter skriver aldri)
- KB-sti-forankring (komprehensiv): 36 bare referanse-subdir-stier i 6 agenter +
  2 commands fullkvalifisert med CLAUDE_PLUGIN_ROOT/skills/<skill>/references/ ---
  ogsa innen-skill kortformer, uresolverbare i installert modus
- mekanisme: validate-plugin.sh Check 6d (bare-subdir-lint m/ hyphen-guard) +
  negativ-probe-test (beviser tenner) + RX-P2 wiring-regresjonstester

Suite 859/859 exit 0. validate-plugin 250 PASS / 0 FAIL. 145 forankrede stier
verifisert eksisterende (0 mangler).
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 12:17:01 +02:00
commit b68514487c
13 changed files with 151 additions and 36 deletions

View file

@ -0,0 +1,45 @@
// RX-P2 regression guards: classify→dpia→ros chain wiring (del 1), removal of the
// unexecutable "ask user" subagent instructions (del 2), and single ADR write-ownership
// (del 4). Path anchoring (del 3) + its lint (del 5) are enforced by validate-plugin.sh
// Check 6d via test-validate-plugin.test.mjs.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, '..', '..'); // tests/kb-update -> plugin root
const read = (rel) => readFileSync(join(root, rel), 'utf8');
// --- Del 1: chain wiring — the Task templates must carry explicit chain-data fields ---
test('dpia.md Task template passes AI Act classification as chain-data', () => {
const dpia = read('commands/dpia.md');
assert.match(dpia, /\*\*AI Act-klassifisering \(fra \/architect:classify/);
});
test('ros.md Task template passes AI Act classification (dim. 6) + DPIA findings as chain-data', () => {
const ros = read('commands/ros.md');
assert.match(ros, /\*\*AI Act-klassifisering \(dimensjon 6/);
assert.match(ros, /\*\*DPIA-funn \(fra \/architect:dpia/);
});
// --- Del 2: no unexecutable "ask the user" instructions in Task-spawned subagents ---
test('dpia-agent.md no longer instructs the subagent to ask the user', () => {
const agent = read('agents/dpia-agent.md');
assert.doesNotMatch(agent, /Spør om det bør gjøres/);
assert.doesNotMatch(agent, /Request specific details needed/);
});
test('ros-analysis-agent.md no longer instructs the subagent to ask the user', () => {
const agent = read('agents/ros-analysis-agent.md');
assert.doesNotMatch(agent, /Request specific details needed/);
});
// --- Del 4: adr-writer-agent returns content; the command is the single writer ---
test('adr-writer-agent.md does not carry the Write tool and does not write files', () => {
const agent = read('agents/adr-writer-agent.md');
const toolsLine = agent.split('\n').find((l) => l.startsWith('tools:')) || '';
assert.doesNotMatch(toolsLine, /Write/, 'adr-writer-agent must not have the Write tool (subagents never write)');
assert.doesNotMatch(agent, /### 4\. Write to File/);
});

View file

@ -6,6 +6,7 @@ import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { writeFileSync, rmSync } from 'node:fs';
const here = dirname(fileURLToPath(import.meta.url));
const pluginRoot = join(here, '..', '..'); // tests/kb-update -> plugin root
@ -23,3 +24,42 @@ test('validate-plugin.sh passes (frontmatter, encoding, KB refs, plugin.json, in
assert.match(out, /VALIDATION PASSED/);
assert.doesNotMatch(out, /VALIDATION FAILED/);
});
// RX-P2: prove Check 6d has teeth — an unanchored reference-subdir path must be caught.
// Without a negative probe, the positive test above only proves the plugin is currently
// clean, not that 6d actually fires.
test('validate-plugin.sh Check 6d catches an unanchored reference-subdir path (negative probe)', () => {
const probe = join(pluginRoot, 'agents', '__rx_p2_6d_probe__.md');
writeFileSync(
probe,
[
'---',
'name: rx-p2-6d-probe',
'description: temporary probe for the 6d negative test',
'model: opus',
'color: blue',
'tools: ["Read"]',
'---',
'',
'# Probe',
'',
'Load: `responsible-ai/ai-act-compliance-guide.md`',
'',
].join('\n'),
);
try {
let failed = false;
let out = '';
try {
out = execFileSync('bash', [script], { encoding: 'utf8', cwd: pluginRoot });
} catch (err) {
failed = true;
out = `${err.stdout || ''}${err.stderr || ''}`;
}
assert.ok(failed, 'validate-plugin.sh must exit non-zero when a bare reference-subdir path is present');
assert.match(out, /bare reference-subdir path/);
assert.match(out, /__rx_p2_6d_probe__\.md/);
} finally {
rmSync(probe, { force: true });
}
});

View file

@ -349,6 +349,33 @@ else
pass "No sonnet/haiku pinned in Task delegations"
fi
# 6d: every bundled reference-subdir path must be ${CLAUDE_PLUGIN_ROOT}-anchored.
# Short-forms like `responsible-ai/foo.md` (a reference subdir + file, without the
# full skills/<skill>/references/ prefix) resolve only in dev cwd -- installed mode
# breaks. A path is anchored iff the subdir is immediately preceded by '/'
# (.../references/<subdir>/). The (^|[^A-Za-z0-9._/-]) guard fires on bare short-forms
# (preceded by backtick/space/comma/colon) while skipping anchored paths (preceded
# by '/') and compound subdir names (e.g. 'architecture' inside 'rag-architecture',
# preceded by '-').
REF_SUBDIRS='architecture|copilot-extensibility|development|platforms|prompt-engineering|agent-orchestration|api-management|azure-ai-services|data-engineering|mlops-genaiops|multi-modal|rag-architecture|monitoring-observability|norwegian-public-sector-governance|responsible-ai|bcdr|hybrid-edge|ai-security-engineering|cost-optimization|performance-scalability'
subdir_offenders=0
for f in "$PLUGIN_ROOT"/commands/*.md "$PLUGIN_ROOT"/agents/*.md; do
[ -f "$f" ] || continue
rel="${f#$PLUGIN_ROOT/}"
bare="$(grep -nE "(^|[^A-Za-z0-9._/-])($REF_SUBDIRS)/[A-Za-z0-9._-]+\.md" "$f" || true)"
if [ -n "$bare" ]; then
while IFS= read -r bline; do
[ -n "$bline" ] || continue
lineno="${bline%%:*}"
fail "$rel:$lineno: bare reference-subdir path (needs \${CLAUDE_PLUGIN_ROOT}/skills/<skill>/references/ prefix for installed mode)"
subdir_offenders=$((subdir_offenders + 1))
done <<< "$bare"
fi
done
if [ "$subdir_offenders" -eq 0 ]; then
pass "All reference-subdir paths are \${CLAUDE_PLUGIN_ROOT}-anchored"
fi
echo ""
# -------------------------------------------------------