Skip to content

Commit 0b97a94

Browse files
committed
Enhance source attribution handling and UI in challenge details
- Remove hardcoded source repo references and implement dynamic source URL generation. - Add source files section in the challenge detail page for better visibility. - Introduce validation for local source attribution in the audit script.
1 parent 48d2e0a commit 0b97a94

5 files changed

Lines changed: 85 additions & 12 deletions

File tree

docs/assets/js/challenge.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22
(function () {
33
'use strict';
44

5-
// Home-repo slugs (this live consolidated repo); render as plain text without a hyperlink.
6-
const SELF_SOURCE_REPOS = new Set([
7-
'microsoft/frontier-agentic-devops-rvas',
8-
]);
9-
105
let _kiosk = null;
116

127
/* Internal challenge link that preserves kiosk state when active */
@@ -91,11 +86,9 @@
9186
// Attribution
9287
const attr = document.getElementById('attribution');
9388
if (attr && c.source_repo) {
94-
if (SELF_SOURCE_REPOS.has(c.source_repo)) {
95-
attr.innerHTML = `Source: ${FP.esc(c.source_repo)} · ${FP.esc(c.license || 'MIT')} License`;
96-
} else {
97-
attr.innerHTML = `Source: <a href="https://github.com/${FP.esc(c.source_repo)}" target="_blank" rel="noopener">${FP.esc(c.source_repo)}</a> · ${FP.esc(c.license || 'MIT')} License`;
98-
}
89+
const sourceUrl = FP.githubSourceUrl(c.source_repo, c.source_path, c.source_ref);
90+
const sourceLabel = FP.esc(c.source_repo + (c.source_path ? ` / ${c.source_path}` : ''));
91+
attr.innerHTML = `Origin: <a href="${sourceUrl}" target="_blank" rel="noopener">${sourceLabel}</a> · ${FP.esc(c.license || 'MIT')} License`;
9992
}
10093
}
10194

@@ -170,6 +163,24 @@
170163
.join('') || '<span class="text-dim" style="font-size:0.8rem">No tags</span>';
171164
}
172165

166+
// Exact source files for the two rendered guide views.
167+
const sourcePanel = document.getElementById('sourcePanel');
168+
const sourceList = document.getElementById('sourceList');
169+
if (sourcePanel && sourceList) {
170+
const sources = [
171+
['Delivery guide', c.student_source_repo, c.student_source_path],
172+
['Delivery assurance', c.coach_source_repo, c.coach_source_path],
173+
].filter(([, repo, sourcePath]) => repo && sourcePath);
174+
175+
if (!sources.length) {
176+
sourcePanel.style.display = 'none';
177+
} else {
178+
sourceList.innerHTML = sources.map(([label, repo, sourcePath]) =>
179+
`<a href="${FP.githubSourceUrl(repo, sourcePath, c.source_ref)}" target="_blank" rel="noopener" class="attribution" style="display:block;margin-bottom:5px">${FP.esc(label)} ↗</a>`
180+
).join('');
181+
}
182+
}
183+
173184
// References
174185
const refPanel = document.getElementById('refPanel');
175186
const refList = document.getElementById('refList');

docs/assets/js/core.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@
7979
FP.catalogOutcomeUrl = function (id) {
8080
return 'catalog.html?outcome=' + encodeURIComponent(id);
8181
};
82+
FP.githubSourceUrl = function (repo, sourcePath, ref) {
83+
const base = 'https://github.com/' +
84+
String(repo || '').split('/').map(encodeURIComponent).join('/');
85+
if (!sourcePath) return base;
86+
return base + '/blob/' + encodeURIComponent(ref || 'main') + '/' +
87+
String(sourcePath).split('/').map(encodeURIComponent).join('/');
88+
};
8289
FP.outcomeName = function (outcomeId, outcomes) {
8390
const o = (outcomes || []).find((x) => x.id === outcomeId);
8491
return o ? o.name : outcomeId;

docs/build.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ const OUT_DATA_DIR = path.join(__dirname, 'assets', 'data');
8181
const OUT_GUIDES_DIR = path.join(OUT_DATA_DIR, 'challenges');
8282
const OUT_RESOURCES_DIR = path.join(__dirname, 'resources');
8383
const OUTCOMES_PATH = path.join(ROOT, 'outcomes.json');
84+
const CURRENT_SOURCE_REPO = 'microsoft/frontier-agentic-devops-rvas';
85+
const CURRENT_SOURCE_REF = process.env.SOURCE_REF || 'main';
8486

8587
/* ─── Minimal YAML parser ────────────────────────────────────────────────────
8688
* Handles only the locked meta.yml contract: scalar key-value pairs, block
@@ -231,6 +233,21 @@ function readDirSafe(p) {
231233
catch { return []; }
232234
}
233235

236+
function relativeSourcePath(file) {
237+
return path.relative(ROOT, file).replace(/\\/g, '/');
238+
}
239+
240+
function validateLocalSourceAttribution(meta, metaPath) {
241+
if (meta.source_repo !== CURRENT_SOURCE_REPO) return null;
242+
243+
const sourcePath = String(meta.source_path || '');
244+
const resolved = path.resolve(ROOT, sourcePath);
245+
if (!sourcePath || !resolved.startsWith(`${ROOT}${path.sep}`) || !fs.existsSync(resolved)) {
246+
return `${relativeSourcePath(metaPath)}: source_path "${sourcePath}" does not resolve in ${CURRENT_SOURCE_REPO}`;
247+
}
248+
return null;
249+
}
250+
234251
function rewriteResourceLinksForPages(text, moduleId) {
235252
const moduleResources = `resources/${moduleId}/`;
236253
return text.replace(
@@ -407,6 +424,11 @@ function main() {
407424

408425
const raw = parseMeta(fs.readFileSync(metaPath, 'utf8'));
409426
const meta = normaliseMeta(raw, moduleId, slug);
427+
const sourceAttributionError = validateLocalSourceAttribution(meta, metaPath);
428+
if (sourceAttributionError) {
429+
console.error(` ✗ ${sourceAttributionError}`);
430+
errors++;
431+
}
410432

411433
// Warn on missing recommended fields
412434
const warnFields = ['description', 'title', 'track', 'difficulty', 'duration_minutes'];
@@ -429,8 +451,8 @@ function main() {
429451
const hasReadme = copyGuideForPages(path.join(dir, 'README.md'), path.join(guideDir, 'README.md'), moduleId, meta.tier !== 'setup');
430452
const hasCoach = copyGuideForPages(path.join(dir, 'COACH.md'), path.join(guideDir, 'COACH.md'), moduleId);
431453

432-
if (!hasReadme) { console.warn(` ! ${meta.id}: no README.md (delivery guide)`); warnings++; }
433-
if (!hasCoach) { console.warn(` ! ${meta.id}: no COACH.md (coach guide)`); warnings++; }
454+
if (!hasReadme) { console.error(` ${meta.id}: no README.md (delivery guide)`); errors++; }
455+
if (!hasCoach) { console.error(` ${meta.id}: no COACH.md (coach guide)`); errors++; }
434456

435457
const trackCfg = (moduleCfg.tracks && moduleCfg.tracks[meta.track]) || {};
436458

@@ -456,6 +478,11 @@ function main() {
456478
references: meta.references.filter(r => r && r !== 'TODO'),
457479
source_repo: meta.source_repo || '',
458480
source_path: meta.source_path || '',
481+
source_ref: CURRENT_SOURCE_REF,
482+
student_source_repo: CURRENT_SOURCE_REPO,
483+
student_source_path: relativeSourcePath(path.join(dir, 'README.md')),
484+
coach_source_repo: CURRENT_SOURCE_REPO,
485+
coach_source_path: relativeSourcePath(path.join(dir, 'COACH.md')),
459486
license: meta.license,
460487
student_path: `assets/data/challenges/${meta.id}/README.md`,
461488
coach_path: `assets/data/challenges/${meta.id}/COACH.md`,

docs/challenge.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ <h1 id="challengeTitle">Loading…</h1>
125125
<div class="panel-body" id="refList"></div>
126126
</div>
127127

128+
<div class="panel" id="sourcePanel">
129+
<div class="panel-head">Source files</div>
130+
<div class="panel-body" id="sourceList"></div>
131+
</div>
132+
128133
<div class="panel" id="relatedPanel">
129134
<div class="panel-head">Related work packages</div>
130135
<div class="panel-body">

scripts/audit-content.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const CHECK_TERMINOLOGY = process.argv.includes('--terminology');
2121
const CHECK_REMOVAL = process.argv.includes('--removal');
2222
const PAGES_HOSTS = new Set(['microsoft.github.io']);
2323
const PROJECT_SLUG = 'frontier-agentic-devops-rvas';
24+
const CURRENT_SOURCE_REPO = `microsoft/${PROJECT_SLUG}`;
2425

2526
const state = {
2627
errors: [],
@@ -264,6 +265,7 @@ function auditGuideSurfaces(challenges) {
264265
if (titleNeedle && h1 && !titleMatchesHeading(titleNeedle, h1)) {
265266
addWarning(rel(readmePath), 1, `README title does not include meta title "${c.meta.title}"`);
266267
}
268+
267269
if (!/^##\s+(Success Criteria|Acceptance Criteria|Verify|Verification)\b/im.test(readme)
268270
&& (!Array.isArray(c.meta.success_criteria) || c.meta.success_criteria.length === 0)) {
269271
addWarning(rel(readmePath), 0, 'delivery guide has no visible success/verification surface');
@@ -278,6 +280,26 @@ function auditGuideSurfaces(challenges) {
278280
}
279281
}
280282

283+
function auditSourceAttribution(challenges) {
284+
for (const c of challenges) {
285+
const fileRel = rel(c.metaPath);
286+
const sourceRepo = String(c.rawMeta.source_repo || '');
287+
const sourcePath = String(c.rawMeta.source_path || '');
288+
289+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(sourceRepo)) {
290+
addError(fileRel, 0, `source_repo must use owner/repository form: "${sourceRepo}"`);
291+
continue;
292+
}
293+
294+
if (sourceRepo !== CURRENT_SOURCE_REPO) continue;
295+
296+
const resolved = path.resolve(ROOT, sourcePath);
297+
if (!sourcePath || !resolved.startsWith(`${ROOT}${path.sep}`) || !fs.existsSync(resolved)) {
298+
addError(fileRel, 0, `source_path does not resolve in this repository: "${sourcePath}"`);
299+
}
300+
}
301+
}
302+
281303
function normalizeText(value) {
282304
return String(value)
283305
.toLowerCase()
@@ -790,6 +812,7 @@ async function main() {
790812
auditMetaContract(challenges);
791813
auditPlaceholders(files);
792814
auditGuideSurfaces(challenges);
815+
auditSourceAttribution(challenges);
793816
auditNumberingGaps(challenges);
794817
auditCodeFencesAndCommands(files);
795818
auditLinks(files);

0 commit comments

Comments
 (0)