From 8b4ea5443554d8ce386fc35c61fd750fce04d86f Mon Sep 17 00:00:00 2001 From: Aleksandr Shevchuk Date: Tue, 10 Nov 2026 18:59:06 +0200 Subject: [PATCH] Copy the type file of a type named as a chat with no skill text names it The copy for the chat carried the type file of the source's `type` as written, so a type named "interactive-book", or under `contentType`, as a chat with no skill text writes it, had none. The page now asks h5p-author 0.8.0 for the source's own type, `sourceType`, the build's reading of it, and below it takes every listed machine name, under any key. A name is read against the list of type files, so `index.json` is fetched first. The 14 rounds that failed in h5p-offline-player#174's test, with no skill text, were built again on this page, and each copied what the model had been given. Of the 39 sources the type-file test replayed before, 36 copy as they did; the other three carry the same type files, and their problems read better since h5p-author 0.7.0 and 0.7.1. Closes #13. --- README.md | 6 ++++-- e2e/build.spec.ts | 40 ++++++++++++++++++++++++++++++++++- package.json | 2 +- pnpm-lock.yaml | 10 ++++----- src/h5p/build/build-page.ts | 8 +++---- src/h5p/build/build-source.ts | 36 +++++++++++++++++++------------ 6 files changed, 75 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 1bb13bb..7973652 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ The page and the bundles were embed-my.github.io's `/build` and `/bundles/` unti ### The type files -A chat in a browser, such as Gemini in Chrome, has the skill's text and none of its files, and can run nothing. It guesses how a content type is written, and each round of problems tells it the names of one level. So **Copy the problems for the chat** copies more than the list: after the problems come the skill's type files of the source's own content type and of the libraries it names, each a type's notes, an example and its fields. +A chat in a browser, such as Gemini in Chrome, has the skill's text and none of its files, and can run nothing. It guesses how a content type is written, and each round of problems tells it the names of one level. So **Copy the problems for the chat** copies more than the list: after the problems come the skill's type files of the source's own content type and of the libraries named below it, each a type's notes, an example and its fields. The source's own type is the one the build takes it to be, h5p-author's `sourceType` (since 0.8.0): its `type` as written or as it is spoken of, `"interactive-book"`, or with none the one under `contentType` or `library`, as a chat with no skill text writes it. Below it, a library counts by its machine name, under any key: a slide's element `{ "type": "text" }` is no H5P.Text. ``` The build page found 2 problems in the source. Fix them all and write the whole source again: @@ -86,7 +86,9 @@ How these content types are written, from the h5p-author skill's type files: `vite.config.ts` writes them to `/h5p/build/types/.md` from the pinned `@missing-elements/h5p-author`, which carries them since 0.7.0, with `index.json`, the author's version and the types there is a file for. They are that release's, so they describe the fields of the build the page runs, and a Dependabot update of the package moves both. The page fetches them when it lists a source's problems, so the copy is written within the button's press; where they cannot be fetched, the problems are copied alone. -Measured in [h5p-offline-player#172](https://github.com/missing-elements/h5p-offline-player/issues/172) on the weakest Claude model, Haiku 5.5, in that position: with the files beside the first problems, 42 of 46 sources of 23 content types were right by its second reply, and 29 without them. The page copies that text byte for byte: the 39 sources that failed their first build in the test's runs were built again on this page, and all but one copied what the model had been given, the one a Game Map whose problems 0.7.0 says better. +Measured in [h5p-offline-player#172](https://github.com/missing-elements/h5p-offline-player/issues/172) on the weakest Claude model, Haiku 5.5, in that position: with the files beside the first problems, 42 of 46 sources of 23 content types were right by its second reply, and 29 without them. And in [#174](https://github.com/missing-elements/h5p-offline-player/issues/174), with no skill text at all, the position of a chat given only a link to the skill: with h5p-author 0.7.1 and the file of a type named as it is spoken of, 10 of 12 sources were right by the second reply and all 12 by the third; with 0.7.0 and the page's rule before, 3 and 8 by the fourth, four never. + +The page copies that text byte for byte. The 14 rounds that failed in the second test were built again on this page, and each copied what the model had been given. Of the 39 sources that failed their first build in the first, 36 did; the other three carry the same type files, and their problems read better since: a Game Map (0.7.0), and two books whose chapters had a title beside them, 13 problems now 7 (0.7.1). ## The bundles diff --git a/e2e/build.spec.ts b/e2e/build.spec.ts index ee91fdc..ec42fbd 100644 --- a/e2e/build.spec.ts +++ b/e2e/build.spec.ts @@ -147,7 +147,7 @@ test('the copy carries the type files of the source’s type and of the librarie await page.locator('#source').fill(JSON.stringify(book)) await page.locator('#build').click() await expect(page.locator('#status')).toHaveAttribute('data-state', 'error') - // The build says what to write, as h5p-author 0.7.0 does. + // The build says what to write, as h5p-author does since 0.7.0. await expect(page.locator('#problems li').first()).toContainText('write params.chapters[0] as { "library": "H5P.Column", "params": { "content": … } }') await expect(page.locator('#copy-types')).toHaveText('The copy also carries the skill’s notes on H5P.InteractiveBook, H5P.AdvancedText and H5P.MultiChoice, with their fields and an example, so the chat does not have to guess.') const [list, files] = (await copied(page)).split(`\n\n${TYPES_HEADING}\n\n`) @@ -166,6 +166,44 @@ test('the copy carries the type files of the source’s type and of the librarie expect(problems, problems.join('\n')).toEqual([]) }) +test('the copy carries the type file of a type named as it is spoken of, or under contentType, as the build reads it', async ({ page, context, request, baseURL }) => { + const { problems } = watch(page, baseURL) + await context.grantPermissions(['clipboard-read', 'clipboard-write']) + const META = { title: 'Physics', language: 'en', license: 'CC BY', authors: [{ name: 'Ann Author', role: 'Author' }] } + /** The type files a source's copy carries after its problems, once a problem of it is shown. */ + const filesOf = async (source: unknown, said: string): Promise => { + await page.goto('/h5p/build/') + await page.locator('#source').fill(JSON.stringify(source)) + await page.locator('#build').click() + await expect(page.locator('#problems')).toContainText(said) + return (await copied(page)).split(`\n\n${TYPES_HEADING}\n\n`)[1] + } + const files = async (...types: string[]) => (await Promise.all(types.map((type) => typeFile(request, type)))).join('\n\n---\n\n') + + // As the weakest model wrote one with no skill text (h5p-offline-player#174): the content's + // fields beside `type`, the type as it is spoken of, a question named by its machine name. + const spoken = { type: 'interactive-book', title: 'Physics', chapters: [{ title: 'Newton', content: [{ type: 'H5P.MultiChoice', question: 'Which law?', answers: [{ text: 'F = ma', correct: true }] }] }] } + expect(await filesOf(spoken, 'takes the machine name, "H5P.InteractiveBook"')).toBe(await files('H5P.InteractiveBook', 'H5P.MultiChoice')) + await expect(page.locator('#copy-types')).toHaveText('The copy also carries the skill’s notes on H5P.InteractiveBook and H5P.MultiChoice, with their fields and an example, so the chat does not have to guess.') + + // As Gemini in Chrome wrote one from a link to the skill: the type under `contentType`, the + // source's own fields in `metadata`, sub-content naming its library the same way. + const gemini = { + contentType: 'H5P.InteractiveBook', + metadata: { title: 'Physics', license: 'CC BY', authors: META.authors }, + params: { chapters: [{ title: 'Newton', content: [{ contentType: 'H5P.AdvancedText', params: { text: '

F = ma

' } }] }] } + } + expect(await filesOf(gemini, 'the content type is its type: write "type": "H5P.InteractiveBook"')).toBe(await files('H5P.InteractiveBook', 'H5P.AdvancedText')) + + // A name the build does not read for the type, under another key, is none; nor below it is a + // text that only reads like one: a slide's element `{ "type": "text" }` is no H5P.Text. + expect(await filesOf({ ...META, contentTypeName: 'interactive book', params: {} }, 'contentTypeName')).toBeUndefined() + await expect(page.locator('#copy-types')).toBeHidden() + const slides = { ...META, type: 'H5P.CoursePresentation', params: { presentation: { slides: [{ elements: [{ type: 'text', x: 5, y: 5, text: 'Newton' }] }] } } } + expect(await filesOf(slides, 'params.presentation.slides[0].elements[0]')).toBe(await files('H5P.CoursePresentation')) + expect(problems, problems.join('\n')).toEqual([]) +}) + test('the problems are copied alone where the type files cannot be fetched, and with them once they can', async ({ page, context, request }) => { await context.grantPermissions(['clipboard-read', 'clipboard-write']) await page.route('**/h5p/build/types/**', (route) => route.fulfill({ status: 503, body: 'unavailable' })) diff --git a/package.json b/package.json index 0f367ba..a28c006 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "vite": "8.3.5" }, "dependencies": { - "@missing-elements/h5p-author": "0.7.0", + "@missing-elements/h5p-author": "0.8.0", "@missing-elements/h5p-libraries": "1.1.0", "@missing-elements/h5p-offline-player": "1.6.0", "@missing-elements/h5p-runtime": "1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9fb2994..3a9fd17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@missing-elements/h5p-author': - specifier: 0.7.0 - version: 0.7.0(@missing-elements/h5p-runtime@1.0.1) + specifier: 0.8.0 + version: 0.8.0(@missing-elements/h5p-runtime@1.0.1) '@missing-elements/h5p-libraries': specifier: 1.1.0 version: 1.1.0 @@ -101,8 +101,8 @@ packages: resolution: {integrity: sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==} engines: {node: '>=18'} - '@missing-elements/h5p-author@0.7.0': - resolution: {integrity: sha512-jjp70nsWCYoajMk+aksd2WXt0LHOGlTclyEzlownETpMssb2Y9r7LCSJsx/36LVVJuEc8ZltNMWHPK2XC58U/g==} + '@missing-elements/h5p-author@0.8.0': + resolution: {integrity: sha512-F85BWeJ3eJsZ8OVgb/MamSb+n/WTXOIauSMQtTq/LX4dJ1Oa03KxKSlmdkl6O0dQdQt25qGx9IGlVLYJLiWxaw==} engines: {node: '>=22'} hasBin: true @@ -1118,7 +1118,7 @@ snapshots: minimatch: 3.1.5 slash: 3.0.0 - '@missing-elements/h5p-author@0.7.0(@missing-elements/h5p-runtime@1.0.1)': + '@missing-elements/h5p-author@0.8.0(@missing-elements/h5p-runtime@1.0.1)': dependencies: '@missing-elements/h5p-libraries': 1.1.0 '@missing-elements/h5p-normalize': 1.3.0 diff --git a/src/h5p/build/build-page.ts b/src/h5p/build/build-page.ts index fbc2952..3023323 100644 --- a/src/h5p/build/build-page.ts +++ b/src/h5p/build/build-page.ts @@ -90,8 +90,7 @@ function fillList(list: HTMLUListElement, items: Problem[]): void { * copied without them. */ async function typeFilesOf(source: unknown): Promise { - const names = typesNamed(source) - if (!names.length) return [] + if (!source || typeof source !== 'object') return [] const fetched = async (path: string): Promise => { const response = await fetch(`${TYPES}${path}`) if (!response.ok) throw new Error(`${path}: ${response.status}`) @@ -99,8 +98,9 @@ async function typeFilesOf(source: unknown): Promise { } try { typeIndex ??= fetched('index.json').then(async (response) => new Set((await response.json() as { types: string[] }).types)) - const known = await typeIndex - return await Promise.all(names.filter((name) => known.has(name)).map(async (type) => { + // The listed types are what a name is read against, as it is or as it is spoken of. + const names = typesNamed(source, await typeIndex) + return await Promise.all(names.map(async (type) => { let text = typeTexts.get(type) if (!text) typeTexts.set(type, text = fetched(`${type}.md`).then((response) => response.text())) return { type, text: await text } diff --git a/src/h5p/build/build-source.ts b/src/h5p/build/build-source.ts index d41f003..3fef3ad 100644 --- a/src/h5p/build/build-source.ts +++ b/src/h5p/build/build-source.ts @@ -4,6 +4,8 @@ * written in, and the text a chat is given back. The browser tests reach it through the page. */ +import { sourceType } from '@missing-elements/h5p-author/browser' + /** A problem or a note, as the author package reports them: where in the source, and what. */ export interface Problem { path: string @@ -110,24 +112,30 @@ export function matchFiles(paths: string[], chosen: File[]): { files: Record): string[] { + const names = new Set(listed) const found = new Set() - const add = (value: unknown): void => { - const name = typeof value === 'string' ? value.trim().split(/\s+/)[0] : '' - if (name) found.add(name) - } + const own = sourceType(source, names) + if (own) found.add(own) const walk = (value: unknown): void => { - if (Array.isArray(value)) value.forEach(walk) - else if (value && typeof value === 'object') { - add((value as { library?: unknown }).library) - Object.values(value).forEach(walk) - } + if (typeof value === 'string') { + // A name is short: a sentence that happens to hold one is not it. + const name = value.length <= 64 ? /^\s*(\S+)(?:\s+\d+\.\d+)?\s*$/.exec(value)?.[1] : undefined + if (name && names.has(name)) found.add(name) + } else if (Array.isArray(value)) value.forEach(walk) + else if (value && typeof value === 'object') Object.values(value).forEach(walk) } - if (source && typeof source === 'object' && !Array.isArray(source)) add((source as { type?: unknown }).type) walk(source) return [...found] }