feat(ms-ai-architect): C3.3 — course_products (live-enumerert) + makeCourseClassifier (TDD) [skip-docs]
Produkt-slug->kategori-mapping for kurs-deteksjon (C3.3 av GODKJENT C3-spec).
- lib/taxonomy.mjs: makeCourseClassifier(tax) — speiler makeClassifier; forste
in-domain produkt vinner; skill AVLEDES fra category_skill (lagres ALDRI i
course_products -> kan ikke divergere); ukjent/tom/non-array -> null.
- data/domain-taxonomy.json: ny topp-niva course_products (slug->category) +
provenance. 15 in-domain live slugs.
- test-taxonomy.test.mjs (+6): atferd (in-domain->{skill,category}, first-wins,
out-of-domain/tom/non-array->null, manglende map) + REELL data-invariant
(hver slug-kategori resolver til skill, derivasjon matcher) + azure-openai-live.
ENUMERERING (gotcha #2, live mot /api/v1/modules?products=<slug> server-side):
- DOD (0 treff): azure-ai-foundry, ai-services, azure-ai-services, foundry ->
Foundry/AI-services-kurs dukker opp under 'azure-openai'.
- Slug-feller bekreftet live: 'fabric' (ikke microsoft-fabric), 'entra' (ikke
microsoft-entra-id), 'azure-cosmos-db' (ikke cosmos-db), 'azure-cognitive-search'
(ikke azure-ai-search).
- 'azure-kubernetes-service' live men EKSKLUDERT (ingen AI-kategori -> stoy).
- Inkluderte (15): azure-openai, azure-machine-learning, azure-cognitive-search,
azure-cosmos-db, fabric, azure-databricks, microsoft-copilot-studio,
power-automate, power-apps, power-platform, ai-builder, microsoft-365-copilot,
microsoft-purview, entra, defender-for-cloud.
Avvik fra spec 4.4-eksempel: course_products lagrer slug->category (ikke
slug->{skill,category}); skill avledes — anti-divergens, encapsulated av
makeCourseClassifier (downstream-kontrakt {skill,category} uendret).
Gate 7 C3.3 mott: test gronn, eval --json IDENTISK (refTall/K-counts uendret).
kb-update 263->269, validate PASSED (239/0), null regresjon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
94c7f42128
commit
b9f51ea7b3
3 changed files with 111 additions and 1 deletions
|
|
@ -4,7 +4,8 @@
|
|||
"provenance": {
|
||||
"category_skill": "PHYSICAL DISK is canonical (skills/<skill>/references/<category>/). category-skill-map.json drifted on 4 entries (copilot-extensibility, monitoring-observability, performance-scalability, prompt-engineering) and is no longer consumed by code.",
|
||||
"sitemap_prefixes": "poll-sitemaps superset (18). discover-new-urls previously had only 12 — now reads this list for poll parity.",
|
||||
"notes": "dotnet_en-us_ deliberately excluded: 75 sitemaps, only ~12 matches — not worth weekly polling."
|
||||
"notes": "dotnet_en-us_ deliberately excluded: 75 sitemaps, only ~12 matches — not worth weekly polling.",
|
||||
"course_products": "Product slugs enumerated LIVE from the Learn Platform API (C3.3, server-side /api/v1/modules?products=<slug> probe). Doc slug 'azure-ai-foundry' is DEAD (0 hits) → Foundry/AI-services courses surface under 'azure-openai'. Slug gotchas confirmed live: 'fabric' (not 'microsoft-fabric'), 'entra' (not 'microsoft-entra-id'), 'azure-cosmos-db' (not 'cosmos-db'), 'azure-cognitive-search' (not 'azure-ai-search'). 'azure-kubernetes-service' is live but excluded (no AI category → noise). Maps slug->category; the owning skill is DERIVED from category_skill via makeCourseClassifier (never stored, cannot diverge)."
|
||||
},
|
||||
"sitemap_prefixes": [
|
||||
"azure_en-us_",
|
||||
|
|
@ -49,6 +50,23 @@
|
|||
"responsible-ai": "ms-ai-governance",
|
||||
"security-scoring": "ms-ai-security"
|
||||
},
|
||||
"course_products": {
|
||||
"azure-openai": "azure-ai-services",
|
||||
"azure-machine-learning": "mlops-genaiops",
|
||||
"azure-cognitive-search": "rag-architecture",
|
||||
"azure-cosmos-db": "data-engineering",
|
||||
"fabric": "data-engineering",
|
||||
"azure-databricks": "data-engineering",
|
||||
"microsoft-copilot-studio": "platforms",
|
||||
"power-automate": "platforms",
|
||||
"power-apps": "platforms",
|
||||
"power-platform": "platforms",
|
||||
"ai-builder": "platforms",
|
||||
"microsoft-365-copilot": "copilot-extensibility",
|
||||
"microsoft-purview": "responsible-ai",
|
||||
"entra": "ai-security-engineering",
|
||||
"defender-for-cloud": "ai-security-engineering"
|
||||
},
|
||||
"relevance": {
|
||||
"include": [
|
||||
{ "pattern": "/azure/ai-foundry/", "category": "azure-ai-services" },
|
||||
|
|
|
|||
|
|
@ -120,6 +120,31 @@ export function makeClassifier(tax) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a course classifier for the Learn Platform API course-detection spore
|
||||
* (C3). course_products maps an in-domain product slug → category; the owning
|
||||
* skill is DERIVED from category_skill (never stored redundantly), exactly as
|
||||
* makeClassifier derives skill for URL classification — so the slug→skill path
|
||||
* cannot diverge from the canonical category ownership. The FIRST in-domain
|
||||
* product in the list wins; a course whose products are all out-of-domain (no
|
||||
* slug in course_products) returns null and is therefore not a lead.
|
||||
* @param {object} tax
|
||||
* @returns {(products: string[]) => ({skill: string|null, category: string}|null)}
|
||||
*/
|
||||
export function makeCourseClassifier(tax) {
|
||||
const map = tax.course_products ?? {};
|
||||
return function classifyCourse(products) {
|
||||
if (!Array.isArray(products)) return null;
|
||||
for (const slug of products) {
|
||||
const category = map[slug];
|
||||
if (category) {
|
||||
return { skill: tax.category_skill[category] ?? null, category };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a file-priority function mirroring report-changes getFilePriority:
|
||||
* ordered regex rules over the lowercased path; first match wins; else default.
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
getSitemapPrefixes,
|
||||
getCategorySkill,
|
||||
makeClassifier,
|
||||
makeCourseClassifier,
|
||||
makePriorityFn,
|
||||
applyCategoryReassignments,
|
||||
} from '../../scripts/kb-update/lib/taxonomy.mjs';
|
||||
|
|
@ -165,3 +166,69 @@ test('saveTaxonomy + loadTaxonomy — atomic round-trip preserves content', () =
|
|||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// --- (f) makeCourseClassifier (C3.3) — product-slug → {skill, category} ---
|
||||
// course_products maps an in-domain Learn product slug → category; the owning
|
||||
// skill is DERIVED from category_skill (never stored redundantly), mirroring
|
||||
// makeClassifier's anti-divergence rule. First in-domain product wins; a course
|
||||
// whose products are all out-of-domain returns null (not a lead).
|
||||
|
||||
// Synthetic taxonomy isolates the classifier's behaviour from the real data.
|
||||
const courseTax = {
|
||||
category_skill: {
|
||||
'azure-ai-services': 'ms-ai-engineering',
|
||||
'platforms': 'ms-ai-advisor',
|
||||
},
|
||||
course_products: {
|
||||
'azure-openai': 'azure-ai-services',
|
||||
'microsoft-copilot-studio': 'platforms',
|
||||
'power-automate': 'platforms',
|
||||
},
|
||||
};
|
||||
|
||||
test('makeCourseClassifier — in-domain slug → derived {skill, category}', () => {
|
||||
const classify = makeCourseClassifier(courseTax);
|
||||
assert.deepEqual(classify(['azure-openai']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' });
|
||||
assert.deepEqual(classify(['microsoft-copilot-studio']), { skill: 'ms-ai-advisor', category: 'platforms' });
|
||||
});
|
||||
|
||||
test('makeCourseClassifier — first in-domain product wins (order matters)', () => {
|
||||
const classify = makeCourseClassifier(courseTax);
|
||||
// a leading out-of-domain slug is skipped; first in-domain wins
|
||||
assert.deepEqual(classify(['some-unknown-product', 'azure-openai']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' });
|
||||
// when two in-domain products are present, the FIRST in the list wins
|
||||
assert.deepEqual(classify(['azure-openai', 'power-automate']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' });
|
||||
assert.deepEqual(classify(['power-automate', 'azure-openai']), { skill: 'ms-ai-advisor', category: 'platforms' });
|
||||
});
|
||||
|
||||
test('makeCourseClassifier — out-of-domain / empty / non-array → null', () => {
|
||||
const classify = makeCourseClassifier(courseTax);
|
||||
assert.equal(classify(['totally-unknown', 'another-unknown']), null, 'all out-of-domain → null');
|
||||
assert.equal(classify([]), null, 'empty products → null');
|
||||
assert.equal(classify(null), null, 'non-array input → null (defensive)');
|
||||
assert.equal(classify(undefined), null, 'undefined input → null (defensive)');
|
||||
});
|
||||
|
||||
test('makeCourseClassifier — taxonomy without course_products → always null', () => {
|
||||
const classify = makeCourseClassifier({ category_skill: {} });
|
||||
assert.equal(classify(['azure-openai']), null, 'absent course_products map → no lead');
|
||||
});
|
||||
|
||||
// --- (g) REAL course_products invariant (C3.3 data, enumerated from live API) ---
|
||||
test('INVARIANT — every course_products category resolves to a skill, derivation matches', () => {
|
||||
assert.ok(tax.course_products, 'course_products section present in domain-taxonomy.json');
|
||||
assert.ok(Object.keys(tax.course_products).length > 0, 'course_products is non-empty');
|
||||
const classify = makeCourseClassifier(tax);
|
||||
for (const [slug, category] of Object.entries(tax.course_products)) {
|
||||
assert.equal(typeof category, 'string', `${slug} maps to a category string`);
|
||||
const skill = getCategorySkill(tax, category);
|
||||
assert.ok(skill, `course_products slug ${slug} → category ${category} absent from category_skill`);
|
||||
assert.deepEqual(classify([slug]), { skill, category }, `divergence for slug ${slug}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('makeCourseClassifier — azure-openai (verified-live slug) → engineering/azure-ai-services', () => {
|
||||
// azure-openai is the empirically-confirmed live slug (spike §: 34 hits).
|
||||
const classify = makeCourseClassifier(tax);
|
||||
assert.deepEqual(classify(['azure-openai']), { skill: 'ms-ai-engineering', category: 'azure-ai-services' });
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue