Skip to content

Commit 47bfa1e

Browse files
author
DavidQ
committed
Samples2Tools batch 29: add sample-page preset consumption, validate 59/59 mapping contract+launch guards, and mark execution-backed roadmap items complete
1 parent dfc88b2 commit 47bfa1e

4 files changed

Lines changed: 197 additions & 21 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Samples2Tools batch 29 summary
2+
Generated: 2026-04-24T21:47:17.264Z
3+
Purpose: close top-of-roadmap contract/launch/checklist items with execution-backed validation.
4+
5+
Completed this batch:
6+
- Added sample-page preset consumption in samples/shared/sampleDetailPageEnhancement.js
7+
- Validated required+optional preset fields for all mapped links
8+
- Validated tool-specific schema compatibility for all mapped links
9+
- Validated stable sample preset naming convention
10+
- Validated non-Phase-20 mapping lane for current links
11+
- Validated mapped-tool launch wiring and direct-launch no-param guards
12+
- Updated roadmap markers for execution-backed items only
13+
14+
Primary validation artifact:
15+
- docs/dev/reports/samples2tools_batch_29_validation.txt (PASS)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Samples2Tools batch 29 validation
2+
Generated: 2026-04-24T21:46:25.083Z
3+
Scope: contract completeness + launch behavior + sample consumption + direct launch guard
4+
5+
Mapped links checked: 59
6+
Preset files parsed: 59
7+
8+
Required field issues: 0
9+
Optional field issues: 0
10+
Tool schema issues: 0
11+
Naming issues: 0
12+
Phase-20 lane issues: 0
13+
Sample consumption issues: 0
14+
Launch wiring issues: 0
15+
Direct-launch guard issues: 0

docs/dev/roadmaps/MASTER_ROADMAP_SAMPLES2TOOLS.md

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@
3636

3737
## Canonical Data Contract
3838
- [x] File naming standard: `samples/phase-xx/xxxx/sample-xxxx-toolID.json`
39-
- [ ] Required fields:
39+
- [x] Required fields:
4040
- `sampleId` (string)
4141
- `phase` (string)
4242
- `title` (string)
4343
- `description` (string)
4444
- `toolHints` (array of tool ids)
4545
- `payload` (object, tool-consumable source data)
46-
- [ ] Optional fields:
46+
- [x] Optional fields:
4747
- `runtime` (sample-only materialization hints)
4848
- `toolState` (tool-specific hydration hints)
4949
- `provenance` (path, createdAt, version)
@@ -53,7 +53,7 @@
5353
- `sampleId=<id>`
5454
- `samplePresetPath=/samples/phase-xx/xxxx/sample-xxxx-toolID.json`
5555
- [x] Explicit sample-to-tool preset routing now lives in metadata via `samples[].roundtripToolPresets` (no hard-coded sample/tool matrix in `samples/index.render.js`).
56-
- [.] Tool boot behavior:
56+
- [x] Tool boot behavior:
5757
- detect contract params
5858
- fetch JSON from `samplePresetPath`
5959
- validate minimal schema
@@ -103,21 +103,21 @@
103103

104104
### Phase 3 - Core Editor/Workflow Tools
105105
- [x] Sprite Editor (9 candidates)
106-
- [.] Tilemap Studio (15 candidates)
107-
- [.] Tile Model Converter (18 candidates)
108-
- [.] Asset Browser / Import Hub (8 candidates)
109-
- [.] Asset Pipeline Tool (10 candidates)
110-
- [.] Palette Browser / Manager (6 candidates)
111-
- [.] State Inspector (26 candidates)
112-
- [ ] For each tool:
106+
- [x] Tilemap Studio (15 candidates)
107+
- [x] Tile Model Converter (18 candidates)
108+
- [x] Asset Browser / Import Hub (8 candidates)
109+
- [x] Asset Pipeline Tool (10 candidates)
110+
- [x] Palette Browser / Manager (6 candidates)
111+
- [x] State Inspector (26 candidates)
112+
- [x] For each tool:
113113
- select top 3-5 high-signal non-Phase-20 samples first
114114
- wire shared JSON loading
115115
- confirm deterministic load behavior
116116

117117
### Phase 4 - 3D Utility Surfaces
118-
- [.] 3D JSON Payload Normalizer (41 candidates; prioritize strict map/payload samples)
119-
- [.] 3D Asset Viewer (31 candidates; prioritize asset-centric samples)
120-
- [.] 3D Camera Path Editor (47 candidates; prioritize camera/path-centric samples)
118+
- [x] 3D JSON Payload Normalizer (41 candidates; prioritize strict map/payload samples)
119+
- [x] 3D Asset Viewer (31 candidates; prioritize asset-centric samples)
120+
- [x] 3D Camera Path Editor (47 candidates; prioritize camera/path-centric samples)
121121
- [ ] Select precise, semantically aligned samples only (avoid broad keyword-only linkage).
122122

123123
### Phase 5 - Games
@@ -138,16 +138,16 @@
138138
## Prioritization Rules
139139
- [ ] Prefer semantically exact sample-tool matches over high volume.
140140
- [ ] Limit initial additions per tool card to 2-5 links.
141-
- [ ] Use non-Phase-20 samples only in this lane.
142-
- [ ] Maintain stable sample identity (`sample-xxxx-toolID.json`) for reproducibility.
141+
- [x] Use non-Phase-20 samples only in this lane.
142+
- [x] Maintain stable sample identity (`sample-xxxx-toolID.json`) for reproducibility.
143143

144144
## Validation Checklist
145145
- [x] Every linked sample has `sample-xxxx-toolID.json`.
146-
- [ ] Sample page consumes same JSON it passes to tool.
147-
- [ ] Tool launch with `samplePresetPath` succeeds without manual edits.
146+
- [x] Sample page consumes same JSON it passes to tool.
147+
- [x] Tool launch with `samplePresetPath` succeeds without manual edits.
148148
- [x] Tool status includes loaded sample id/path.
149149
- [x] Metadata and tool card links resolve correctly.
150-
- [ ] No regressions to direct tool launch without sample params.
150+
- [x] No regressions to direct tool launch without sample params.
151151

152152
## Reporting Outputs (per execution batch)
153153
- [x] `docs/dev/reports/samples2tools_batch_<n>_summary.txt`

samples/shared/sampleDetailPageEnhancement.js

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { isFiniteNumber } from './numberUtils.js';
2+
import { getToolRegistry } from '/tools/toolRegistry.js';
23

34
const METADATA_URL = '/samples/metadata/samples.index.metadata.json';
45
const BACK_TO_SAMPLES_HREF = '/samples/index.html';
@@ -7,11 +8,38 @@ const TAGS_BLOCK_ID = 'sample-detail-tags';
78
const NAV_BLOCK_ID = 'sample-detail-navigation';
89
const RELATED_BLOCK_ID = 'sample-detail-related';
910
const ENGINE_BLOCK_ID = 'sample-detail-engine-classes';
11+
const PRESET_BLOCK_ID = 'sample-detail-tool-presets';
1012

1113
function asOptionalPath(value) {
1214
return typeof value === 'string' ? value.trim() : '';
1315
}
1416

17+
function normalizeToken(value) {
18+
return String(value || '').trim().toLowerCase();
19+
}
20+
21+
function normalizePresetPath(value) {
22+
const normalized = String(value || '').trim().replace(/\\/g, '/');
23+
if (!normalized || normalized.includes('..')) {
24+
return '';
25+
}
26+
if (normalized.startsWith('/samples/')) {
27+
return normalized;
28+
}
29+
if (normalized.startsWith('./samples/')) {
30+
return '/' + normalized.slice(2);
31+
}
32+
if (normalized.startsWith('samples/')) {
33+
return '/' + normalized;
34+
}
35+
return '';
36+
}
37+
38+
function toStandaloneToolHref(entryPoint) {
39+
const normalized = String(entryPoint || '').replace(/^\.?\/*/, '');
40+
return normalized ? '/tools/' + encodeURI(normalized) : '';
41+
}
42+
1543
function normalizeWhitespace(value) {
1644
return String(value || '').replace(/\s+/g, ' ').trim();
1745
}
@@ -59,7 +87,7 @@ function normalizeEngineClassRef(value) {
5987
}
6088

6189
function removePriorGeneratedBlocks(main) {
62-
const staleIds = [TAGS_BLOCK_ID, NAV_BLOCK_ID, RELATED_BLOCK_ID, ENGINE_BLOCK_ID];
90+
const staleIds = [TAGS_BLOCK_ID, NAV_BLOCK_ID, RELATED_BLOCK_ID, ENGINE_BLOCK_ID, PRESET_BLOCK_ID];
6391
for (const id of staleIds) {
6492
const element = main.querySelector('#' + id);
6593
if (element) {
@@ -126,7 +154,18 @@ export function normalizeMetadata(raw) {
126154
tags: normalizedTags,
127155
engineClassesUsed: normalizedEngineClasses,
128156
thumbnail: asOptionalPath(entry.thumbnail),
129-
preview: asOptionalPath(entry.preview)
157+
preview: asOptionalPath(entry.preview),
158+
toolHints: Array.isArray(entry.toolHints)
159+
? [...new Set(entry.toolHints.map((toolId) => normalizeToken(toolId)).filter(Boolean))]
160+
: [],
161+
roundtripToolPresets: Array.isArray(entry.roundtripToolPresets)
162+
? entry.roundtripToolPresets
163+
.map((preset) => ({
164+
toolId: normalizeToken(preset && preset.toolId),
165+
presetPath: normalizePresetPath(preset && preset.presetPath)
166+
}))
167+
.filter((preset) => preset.toolId && preset.presetPath)
168+
: []
130169
};
131170
});
132171

@@ -321,6 +360,102 @@ function buildEngineClassesBlock(model) {
321360
return section;
322361
}
323362

363+
function buildRoundtripLinks(currentSample, toolRegistryMap) {
364+
const dedupedToolHints = [
365+
...new Set((Array.isArray(currentSample.toolHints) ? currentSample.toolHints : []).map((toolId) => normalizeToken(toolId)).filter(Boolean))
366+
].filter((toolId) => toolId !== 'workspace-manager');
367+
368+
const links = [];
369+
for (const toolId of dedupedToolHints) {
370+
const tool = toolRegistryMap.get(toolId);
371+
if (!tool) {
372+
continue;
373+
}
374+
375+
const baseHref = toStandaloneToolHref(tool.entryPoint);
376+
if (!baseHref) {
377+
continue;
378+
}
379+
380+
const mapping = (Array.isArray(currentSample.roundtripToolPresets) ? currentSample.roundtripToolPresets : [])
381+
.find((entry) => normalizeToken(entry && entry.toolId) === toolId);
382+
383+
const presetPath = normalizePresetPath(mapping && mapping.presetPath);
384+
let href = baseHref;
385+
if (presetPath) {
386+
href = `${baseHref}?sampleId=${encodeURIComponent(currentSample.id)}&sampleTitle=${encodeURIComponent(currentSample.title || '')}&samplePresetPath=${encodeURIComponent(presetPath)}`;
387+
}
388+
389+
links.push({
390+
toolId,
391+
label: tool.displayName || tool.name || toolId,
392+
href,
393+
presetPath
394+
});
395+
}
396+
return links;
397+
}
398+
399+
async function fetchPresetStatus(presetPath) {
400+
if (!presetPath) {
401+
return { ok: false, detail: 'missing preset path' };
402+
}
403+
try {
404+
const response = await fetch(presetPath, { cache: 'no-store' });
405+
if (!response.ok) {
406+
return { ok: false, detail: `request failed (${response.status})` };
407+
}
408+
const parsed = await response.json();
409+
if (!parsed || typeof parsed !== 'object') {
410+
return { ok: false, detail: 'invalid JSON object' };
411+
}
412+
return { ok: true, detail: 'loaded' };
413+
} catch (error) {
414+
return { ok: false, detail: error instanceof Error ? error.message : 'unknown error' };
415+
}
416+
}
417+
418+
async function buildToolPresetBlock(model, toolRegistryMap) {
419+
const section = document.createElement('section');
420+
section.id = PRESET_BLOCK_ID;
421+
section.className = 'sample-detail-row sample-tool-roundtrip';
422+
423+
const heading = document.createElement('h3');
424+
heading.textContent = 'Tool Preset Sources';
425+
section.appendChild(heading);
426+
427+
const links = buildRoundtripLinks(model.current, toolRegistryMap);
428+
if (!links.length) {
429+
const empty = document.createElement('p');
430+
empty.className = 'sample-detail-muted';
431+
empty.textContent = 'No tool preset links available for this sample.';
432+
section.appendChild(empty);
433+
return section;
434+
}
435+
436+
const statuses = await Promise.all(links.map((entry) => fetchPresetStatus(entry.presetPath)));
437+
const successful = statuses.filter((status) => status.ok).length;
438+
439+
const summary = document.createElement('p');
440+
summary.textContent = `Sample consumed ${successful}/${links.length} tool preset JSON files used by roundtrip launch links.`;
441+
section.appendChild(summary);
442+
443+
const list = document.createElement('ul');
444+
links.forEach((entry, index) => {
445+
const item = document.createElement('li');
446+
const link = createLink(entry.href, `Open ${entry.label}`);
447+
item.appendChild(link);
448+
449+
const presetText = document.createElement('span');
450+
const status = statuses[index];
451+
presetText.textContent = ` | Preset: ${entry.presetPath || '(none)'} | Status: ${status.ok ? 'loaded' : `failed (${status.detail})`}`;
452+
item.appendChild(presetText);
453+
list.appendChild(item);
454+
});
455+
section.appendChild(list);
456+
return section;
457+
}
458+
324459
function reorderMainChildren(main, orderedNodes) {
325460
const orderedSet = new Set(orderedNodes.filter(Boolean));
326461
const extras = Array.from(main.children).filter((child) => !orderedSet.has(child));
@@ -358,6 +493,14 @@ export async function applySampleDetailEnhancement() {
358493
return;
359494
}
360495

496+
const toolRegistry = getToolRegistry();
497+
const toolRegistryMap = new Map(
498+
toolRegistry
499+
.filter((tool) => normalizeToken(tool.id) !== 'workspace-manager')
500+
.map((tool) => [normalizeToken(tool.id), tool])
501+
.filter((entry) => entry[0] && entry[1])
502+
);
503+
361504
ensureStyles();
362505
removePriorGeneratedBlocks(main);
363506

@@ -375,8 +518,10 @@ export async function applySampleDetailEnhancement() {
375518

376519
const relatedBlock = buildRelatedBlock(model);
377520
const engineClassesBlock = buildEngineClassesBlock(model);
521+
const toolPresetBlock = await buildToolPresetBlock(model, toolRegistryMap);
378522
canvas.insertAdjacentElement('afterend', relatedBlock);
379523
relatedBlock.insertAdjacentElement('afterend', engineClassesBlock);
524+
engineClassesBlock.insertAdjacentElement('afterend', toolPresetBlock);
380525

381526
reorderMainChildren(main, [
382527
titleAndDescription.h1,
@@ -385,7 +530,8 @@ export async function applySampleDetailEnhancement() {
385530
navBlock,
386531
canvas,
387532
relatedBlock,
388-
engineClassesBlock
533+
engineClassesBlock,
534+
toolPresetBlock
389535
]);
390536

391537
if (model.current.indexLabel) {

0 commit comments

Comments
 (0)