Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion design/author.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ already holds text to the content check's rules, from the pack an untrusted chec
`apps/skills` plays every example untrusted too. Measured on a five-question quiz, at four
characters a token: `content.json` was about 3,470 tokens to write by hand, after reading 3,900 to
9,500 of a type's semantics; the source is 1,000 to 1,800 tokens, written from a type file of 200
to 3,000. Nineteen things the code does not say by itself:
to 3,000. Twenty-four things the code does not say by itself:

- **One generator, three ways to run it.** The third, `saved`, checks content as H5P stores it
(*Saved content is checked as the build checks what it writes*, below). `ContentGenerator` was the sweep's
Expand Down Expand Up @@ -583,6 +583,51 @@ to 3,000. Nineteen things the code does not say by itself:
gaps, the two checks report nothing; over the 524 distinct sources these tests stored, 199 of
the two types with 765 gaps, only the 14 known to play wrong. Not tried: a text edited afterwards in H5P's own editor, whose rich-text
widget may write an entity back as its character.
- **The hand-over carries the source, its link to the build page and where to say how it went**
(2026-11-12, #190). The owner wants people to try the skill and to hear from them. The build
page asks how it went, by email first, since a teacher may have no GitHub account, or in a
GitHub discussion; but a chat that builds the package itself hands it over in the chat, and its
user never sees that page. The organisation's site counts no visits, and the repository's
discussions held no feedback. So step 5 hands over, beside the package, the source as a file
under the package's name with `.source.json`, which the page opens too, and both lines step 3's
command prints: the page's link that carries the source and, under it, where to say how it
went, `https://missing-elements.github.io/h5p/build/#feedback-heading`, the page's own section,
so where feedback goes stays the site's to change, in one place. That address is a contract
with every installed copy, as `#source=` is: the page keeps the id. A fragment with no `source`
builds nothing (`sourceFromFragment` reads `source` alone), and in Chromium, 1280 and 390 px
wide, the section is in view once the page has settled.

Measured on Haiku in Claude Code's place (the skill, a shell, npm, the verifier left out), a
three-question Question Set with the author and licence given. With 0.9.4's text, 2 of 3 runs
handed the source over as a file, unasked, and none a link or where to say how it went. With
the new text, all 14 runs handed over the source file and a link that decodes to the source
byte for byte, 1,230 to 1,460 characters for three questions. Where to say how it went was the
hard part, since a weak model shortens step 5's list as it writes it out: as an item of the
list, last or third, it reached 3 of 6 replies; told to end the reply with it, 3 of 4; printed
by step 3's command under the link, 3 of 4, and the fourth sent the user to the link's page to
say how it went, where every other miss said nothing. The command is kept: what a command
printed is what a weak model hands over, and the line goes wherever the link goes, the bundle
route included. With step 5 saying the line goes in every case, it reached 5 of 5, two plain
sources and three with a picture.

A link carries the text of a source and not the files it names (cubic's review): the page asks
for those by name once it has the source, and a picture at a web address reaches it only where
its host lets another site fetch it. So for a source that names a file, by any `path`, the
commands print the feedback line alone, and in the build page's route such a source goes over
whole, to be pasted, its files chosen on the page. It is decided by the source and not by its
type: nearly every type takes media, Multiple Choice, Question Set, Column, a book and slides
among them, and most sources name none. The source file goes over as before, the files it names
said by name, since a build reads them beside it; they are not copied, a video being large.
When the commands printed a line saying why there was no link, above the feedback line, 3 of 3
runs with a picture passed on neither: they wrote the reason in their own words and left the
address out. With the feedback line printed alone, 3 of 3 passed it on, none gave a link, and 2
of 3 named the picture.
- **The skill names no product** (2026-11-12, the owner's call). Its routes were told apart by
the apps they were measured in; they now say what each needs: a shell that reaches the npm
registry, Node and a download where npm cannot install, or nothing that runs or fetches. What a
name carried is kept as what an agent can observe: the registry's errors, a sandbox that
downloads only what a browsing tool has opened, a code tool that can be unavailable for hours.
This file keeps the apps' names where it records what was measured in them.

Not built: editing an existing package, which a tool beside it does with `checkContent` as its
check; layout for Drag and Drop and Crossword; and any judgement of whether content is right, which
Expand Down
38 changes: 23 additions & 15 deletions skills/h5p-author/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,11 @@ from what you know of the type: the build says, for each place that is wrong, wh

Build the best way this environment allows, and tell the user which you take:

- **Here, with npm**: a shell that reaches the npm registry (Claude Code, a terminal). The
command below.
- **Here, from two files**: Node runs and files can be fetched, but npm cannot install, as in
ChatGPT. *If it cannot install here*, below.
- **In the user's browser**: nothing here runs, or nothing fetches, as in Gemini or a chat with
no tools. *The build page*, below, with no attempt here first.
- **Here, with npm**: a shell that reaches the npm registry, as in a terminal. The command below.
- **Here, from two files**: Node runs and files can be fetched, but npm cannot install, as in a
chat's sandbox that reaches the web and no registry. *If it cannot install here*, below.
- **In the user's browser**: nothing here runs, or nothing fetches, as in a chat with no tools
or one that cannot open a web address. *The build page*, below, with no attempt here first.

Take the first that applies, and the next if it fails.

Expand All @@ -126,9 +125,10 @@ npx @missing-elements/h5p-author@latest build source.json course.h5p
```

**If it cannot install here, build from files instead.** No network for npm (`EAI_AGAIN`,
`ENOTFOUND`, a registry's 403, 404 or 503, as in ChatGPT): do not retry, try another way to
install it, or end with commands for the user to run once npm works. Where Node runs and you can
fetch a file (in ChatGPT, open each address with the browsing tool, then download it), fetch the
`ENOTFOUND`, a registry's 403, 404 or 503): do not retry, try another way to install it, or end
with commands for the user to run once npm works. Where Node runs and you can fetch a file (where
the sandbox downloads only what a browsing tool has opened, open each address with that tool
first, then download it), fetch the
build tool and the bundle of the source's own `type`, the libraries a build of that type reads,
at the addresses https://missing-elements.github.io/h5p/skill/ lists for them. In both addresses
and commands below, `<type>` is that machine name, such as `H5P.Blanks`: another type's bundle
Expand All @@ -153,20 +153,21 @@ for the user to play it before using it.
**Where nothing can be fetched or run, the build page builds it.** No shell, no Node, or no
download: [the build page](https://missing-elements.github.io/h5p/build/) builds the same package in the user's
browser from the source alone, with no install. Give them a link that carries the source, written
with Node or Python, which need no network:
with Node or Python, which need no network, and the line under it, where to say how it went. A
source that names files gets that line alone: a link carries its text, not the files:

```bash
node -e "console.log('https://missing-elements.github.io/h5p/build/#source=' + require('zlib').deflateRawSync(require('fs').readFileSync('source.json')).toString('base64url'))"
python3 -c "import base64,zlib;c=zlib.compressobj(9,zlib.DEFLATED,-15);d=c.compress(open('source.json','rb').read())+c.flush();print('https://missing-elements.github.io/h5p/build/#source='+base64.urlsafe_b64encode(d).decode().rstrip('='))"
node -e "const s=require('fs').readFileSync('source.json'),f=[];JSON.parse(s,(k,v)=>(k==='path'&&typeof v==='string'&&f.push(v),v));f.length||console.log('https://missing-elements.github.io/h5p/build/#source='+require('zlib').deflateRawSync(s).toString('base64url'));console.log('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
python3 -c "import base64,json,zlib;s=open('source.json','rb').read();f=[];json.loads(s,object_hook=lambda o:(isinstance(o.get('path'),str) and f.append(o['path']),o)[1]);c=zlib.compressobj(9,zlib.DEFLATED,-15);d=c.compress(s)+c.flush();f or print('https://missing-elements.github.io/h5p/build/#source='+base64.urlsafe_b64encode(d).decode().rstrip('='));print('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
Comment on lines +160 to +161

@cubic-dev-ai cubic-dev-ai Bot Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: f suppresses the link for every string path, including supported YouTube/Vimeo URLs that the browser builder keeps without downloading media. Preserve the link for URL-only media; the source fragment contains everything needed, so users otherwise have to paste it manually.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At skills/h5p-author/SKILL.md, line 160:

<comment>`f` suppresses the link for every string `path`, including supported YouTube/Vimeo URLs that the browser builder keeps without downloading media. Preserve the link for URL-only media; the source fragment contains everything needed, so users otherwise have to paste it manually.</comment>

<file context>
@@ -153,20 +153,21 @@ for the user to play it before using it.
 ```bash
-node -e "console.log('https://missing-elements.github.io/h5p/build/#source=' + require('zlib').deflateRawSync(require('fs').readFileSync('source.json')).toString('base64url'));console.log('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
-python3 -c "import base64,zlib;c=zlib.compressobj(9,zlib.DEFLATED,-15);d=c.compress(open('source.json','rb').read())+c.flush();print('https://missing-elements.github.io/h5p/build/#source='+base64.urlsafe_b64encode(d).decode().rstrip('='));print('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
+node -e "const s=require('fs').readFileSync('source.json'),f=[];JSON.parse(s,(k,v)=>(k==='path'&&typeof v==='string'&&f.push(v),v));f.length||console.log('https://missing-elements.github.io/h5p/build/#source='+require('zlib').deflateRawSync(s).toString('base64url'));console.log('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
+python3 -c "import base64,json,zlib;s=open('source.json','rb').read();f=[];json.loads(s,object_hook=lambda o:(isinstance(o.get('path'),str) and f.append(o['path']),o)[1]);c=zlib.compressobj(9,zlib.DEFLATED,-15);d=c.compress(s)+c.flush();f or print('https://missing-elements.github.io/h5p/build/#source='+base64.urlsafe_b64encode(d).decode().rstrip('='));print('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"

</file context>


</details>

```suggestion
node -e "const s=require('fs').readFileSync('source.json'),f=[];JSON.parse(s,(k,v)=>(k==='path'&&typeof v==='string'&&f.push(v),v));(!f.length||f.every(p=>/^https?:\\/\\//i.test(p)))&&console.log('https://missing-elements.github.io/h5p/build/#source='+require('zlib').deflateRawSync(s).toString('base64url'));console.log('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
python3 -c "import base64,json,zlib;s=open('source.json','rb').read();f=[];json.loads(s,object_hook=lambda o:(isinstance(o.get('path'),str) and f.append(o['path']),o)[1]);c=zlib.compressobj(9,zlib.DEFLATED,-15);d=c.compress(s)+c.flush();all(p.lower().startswith(('http://','https://')) for p in f) and print('https://missing-elements.github.io/h5p/build/#source='+base64.urlsafe_b64encode(d).decode().rstrip('='));print('How did it go? Say so by email or on GitHub: https://missing-elements.github.io/h5p/build/#feedback-heading')"
Fix with cubic

```

**A link is only what one of these commands printed here, handed over whole: no command run, no
link.** Never write a `#source=` link yourself, from memory or after an example, even when asked
for one: its text is compressed, so it cannot be typed, and one wrong character leaves the page
nothing to build.

With neither, or no tool that runs anything at all (in ChatGPT, data analysis can be unavailable
for hours), give no link: the page sees only what the user pastes, so put the **whole source in
Where they print no link, with neither, or with no tool that runs anything at all (a chat's code
tool can be unavailable for hours), give no link: the page sees only what the user pastes, so put the **whole source in
your reply**, in one `json` code block, and tell them to paste it into the page. Never give the
page's address without the source, in the link or beside it: it builds nothing. The page lists
every problem by its path, with a button that copies them for the chat: fix them all and give a
Expand Down Expand Up @@ -212,7 +213,14 @@ do it: the person's own play is the check, and a script of your own costs more t

Deliver:

- the `.h5p` and the source;
- the `.h5p`, and its source as a file beside it, under the package's name with `.source.json`
(`course.source.json`), copied as the package was; the build page opens it too. Name the files
it names, if any: a build reads them beside it;
- what one of step 3's two commands printed here for this source, handed over whole: the build
page's link that carries it, where the user plays the package in any browser, changes the source
and builds it again (none for a source that names files, which a link cannot carry), and in
every case the line where they can say how it went. A link is only what one of them printed;
where neither runs, there is none;
- `report.json`, `screenshot.png` and the report's full `revision`, from the run that passed;
where it could not run, no report and call the package **unverified**; where it ran but failed,
give its report and reasons and say the package **failed verification**;
Expand Down
Loading