Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions demo/northwind-traders/admin/model/ns.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ schema:Order spin:constructor :OrderConstructor .
dct:title [ a xsd:string ] ;
schema:identifier [ a xsd:string ] ;
schema:orderDate [ a xsd:date ] ;
schema:orderStatus schema:OrderProcessing ;
schema:customer [ a schema:Corporation ] ;
schema:broker [ a schema:Person ] ;
schema:orderedItem [ a schema:OrderItem ] ;
Expand Down
7 changes: 7 additions & 0 deletions packages/editor/taxonomy/ns.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ skos:Concept spin:constructor :ConceptConstructor ;
# xsd:string annotation next to one showing en.
:ConceptConstructor a ldh:Constructor ;
rdfs:label "Concept constructor" ;
dct:title "Concept constructor" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
Expand Down Expand Up @@ -69,6 +70,7 @@ skos:narrower ldh:view :NarrowerConcepts .

:SelectNarrowerConcepts a sp:Select ;
rdfs:label "Select narrower concepts" ;
dct:title "Select narrower concepts" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

Expand Down Expand Up @@ -99,6 +101,7 @@ skos:broader ldh:view :BroaderConcepts .

:SelectBroaderConcepts a sp:Select ;
rdfs:label "Select broader concepts" ;
dct:title "Select broader concepts" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

Expand All @@ -123,6 +126,7 @@ skos:Collection spin:constructor :CollectionConstructor ;

:CollectionConstructor a ldh:Constructor ;
rdfs:label "Collection constructor" ;
dct:title "Collection constructor" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
Expand Down Expand Up @@ -150,6 +154,7 @@ skos:member ldh:view :CollectionMembers .

:SelectCollectionMembers a sp:Select ;
rdfs:label "Select collection members" ;
dct:title "Select collection members" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

Expand All @@ -171,6 +176,7 @@ skos:ConceptScheme spin:constructor :ConceptSchemeConstructor .

:ConceptSchemeConstructor a ldh:Constructor ;
rdfs:label "Concept scheme constructor" ;
dct:title "Concept scheme constructor" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
Expand Down Expand Up @@ -254,6 +260,7 @@ skos:inScheme ldh:inverseView :ConceptsInScheme , :OrphanConcepts .

:SelectConceptsInScheme a sp:Select ;
rdfs:label "Select concepts in scheme" ;
dct:title "Select concepts in scheme" ;
sp:text """
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>

Expand Down
5 changes: 5 additions & 0 deletions screencast/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ node_modules/
tracks/
shots/
docs/out/
# the vector mock's captures: written by scenes/capture-*.mjs from the running dataspace
mock/states/
mock/templates/
mock/assets/*.png
mock/assets/*.json
36 changes: 36 additions & 0 deletions screencast/FINDINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,42 @@ the top; it reads the column instead.

---

## 13. The create modal's add-property row fails for a resource property

**Where** the add-instance modal (`button.add-instance` on a derived view, e.g. Direct
reports on `/employees/2/` constructing a `schema:Person`), foot row `.ldh-prop-addrow`.

**What happens** Selecting `areaServed` (a resource property, `rdfs:range schema:City`)
and pressing Add shows "The form could not be loaded — the page ran into an unexpected
error" and adds no row. Selecting `type` in the same modal adds a row. The same select and
Add on the **edit** form of an existing Person (`/employees/7/`) adds an `areaServed` row
with a lookup combobox as expected.

**Expected** the create modal behaves like the edit form: one more lookup row.

**Cost** a hire with four territories cannot be made in one form; scene 02 assigns one in
the modal and the other three on the new record's edit form.

---

## 14. Create-from-view ends in two different places, and the view it came from does not refresh

**Where** `button.add-instance` on derived views: Cities in this region (`schema:City`),
Products in this category (`schema:Product`), Orders from this customer (`schema:Order`).

**What happens** Saving a City navigates to the new document. Saving a Product or an Order
closes the modal and leaves the page where it was — with the view still showing the old
count ("Total results 12" after the thirteenth product was created); the new row appears
only after the page is loaded again.

**Expected** one behaviour for all constructors, and the view that offered the Create
button re-reading itself after a successful save — that is the loop the button exists for.

**Cost** scenes 11 and 13 re-read their list by clicking the page's own title link; the
rig waits for either the navigation or the modal closing (`createFromView`).

---

## Not reproduced / not attributed

- **`Terminated with [object DocumentFragment]`** appears twice on essentially every
Expand Down
7 changes: 7 additions & 0 deletions screencast/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ docs-publish:
printf "%s %s\n" "$$(sha1sum "$$dest" | cut -d' ' -f1)" "$${dest#$(DOCS_DEST)/}"; \
done

# The supercut: one-gesture shots cut from the gesture takes (scenes/supercut-*.mjs)
# between their <id>-start / <id>-end beats, captioned and joined. SUPERCUT names the
# shot list.
SUPERCUT ?= render/supercut.json
supercut:
node render/supercut.mjs $(SUPERCUT)

mp4:
@shopt -s nullglob; \
for w in tracks/*.webm; do \
Expand Down
136 changes: 132 additions & 4 deletions screencast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ The five scenes, in the order they answer best:

| Scene | opens on | question |
|---|---|---|
| `02-where-we-have-nobody` | `/territories/`, 53 pins | which territories have no sales rep? |
| `02-where-we-have-nobody` | `/territories/`, 53 pins | which territories have no sales rep? — then a hire covers them, and the map is read again |
| `04-is-our-beverages-theirs` | `/categories/`, eight photographs | does a published vocabulary mean what we mean? |
| `06-worked-from-london` | `/employees/`, nine faces | which US territories are worked from London? |
| `06-worked-from-london` | `/employees/`, nine faces | which US territories are worked from London? — then one is handed to Seattle |
| `07-one-rep-for-the-south` | `/territories/`, 53 pins | how thinly is each region staffed? |
| `08-late-customer-or-shipper` | a late order in Graph mode | do late orders follow the customer or the carrier? |
| `08-late-customer-or-shipper` | a late order in Graph mode | do late orders follow the customer or the carrier? — then the order is marked delivered and the chart re-read |
| `09-opening-houston` | `/territories/`, 53 pins | a new territory, created from the region's own list |
| `10-the-dearest-thing` | `/categories/`, eight photographs | the dearest product's bare page, documented from outside |
| `11-a-new-order` | a customer's page, their orders listed | an order booked from the customer's own list; the list read again |
| `13-a-new-product` | `/categories/`, eight photographs | a product created from the category's own list; the list read again |
| `14-reporting-lines` | `/employees/`, nine faces | one reporting link re-pointed; both managers' lists read again |
| `16-a-territory-where-the-orders-are` | `/territories/`, 53 pins | US deliveries with no territory, charted; a territory opened where most land; the chart read again |

Each of those five writes a block saying *why* the opening state was on screen and what
it could not answer — a strong first frame the rest of the clip never refers back to is
Expand All @@ -48,7 +52,10 @@ make docs-publish # optimise docs/out/ into ../docs/
```

Nothing is hardcoded: origin, keystore, password and the `ldh` binary are all
parameters, with the Makefile carrying overridable defaults.
parameters, with the Makefile carrying overridable defaults. The off-camera fixture
resets shell out to `ldh`, so it must be on PATH (`../../LinkedDataHub/cli/bin`) or named
by `LDH_BIN`; a scene whose reset fails stops before the browser opens rather than
filming last take's leftovers.

## Output

Expand All @@ -66,6 +73,23 @@ forward and nothing is shown out of order. Where to cut is measured, not chosen
a scene fires its first beat the instant its opening view has painted, and the trim
lands 0.3s before it.

`render/stitch.mjs` turns a scroll a take made into ground the camera can cross, for a
take that jumped: a browser's own scroll is a 300 ms glide, but its frames are one page
at successive offsets, so the script measures each frame's shift, lays the rows into one
tall still (`tracks/<take>.scroll.png`, fixed chrome dropped) and ends it on the frame
the cut resumes from; the cutter reads a `.png` track as a still whose `from`/`to` span
the pan in seconds. The compose take no longer needs it: the scene glides the form up
with `easeScrollTo` before the Title click and `easeScrollTop` before the mode switch and
the drag, so the camera rides the page's own scroll from the query editor down to Save.

Shots that continue one take abut exactly: a shot's lead (`leadSeconds`) and trail
(`trailSeconds`) overlap its neighbour by 0.2 s, so a continuing shot carries
`fromOffset: 0.2` and the join lands on the frame the previous shot ended on. A shot's
`startFocus` or `focus` box may carry its own `minZoom`/`maxZoom`/`pad`, so a shot can
start on the tight frame its predecessor held and land on a wider one. Every framing
runs on zoompan now — the crop filter's size is fixed for a stream, so it could slide
but never zoom, and its rounding differed from zoompan's by a pixel at a join.

`render/review.mjs` answers the three questions worth asking: did every beat happen
(an aborted run does not report green), did the page end up rich, and does anything
embed an `ldh:Object` inside another one. `--sheet` builds a contact sheet from the
Expand Down Expand Up @@ -197,3 +221,107 @@ that every baked hash resolves.
locator can drive a tab nobody is looking at — see `lib/dom.mjs`.

`FINDINGS.md` records the product defects these scenes ran into.

## Supercut

`make supercut` cuts `tracks/supercut.mp4` (every shot) and `tracks/supercut-spine.mp4`
(the shots flagged `spine`) from the gesture takes, following `render/supercut.json`: each
shot is the stretch of a track between its `<id>-start` and `<id>-end` beats, led in by
0.3 s and stopped 0.1 s short (a beat and its gesture are the same instant), capped from
the front at `maxSeconds` (shifted by `fromOffset` / `toOffset` seconds — a negative
`toOffset` ends a shot before a helper's dead settle), framed on the component the beat's
`focus` box names — `move: "push"` closes in from the full frame over the whole shot,
`"static"` holds the box's frame throughout, `"pan"` moves from the start beat's box to the
end beat's over `moveSeconds` (1.4 s by default — a move between blocks is gradual, never
a jump) at `moveAt` (negative counts back from the gesture's end, where the new block has
just appeared); zoom between `minZoom` (1.4) and `maxZoom` (2.4)
per shot, anchored on the box's top part; every beat carries the drawn cursor's position
(`pointer`, added by the harness), which shapes the frame only for a shot with
`keepPointer: true` (unioning it into every frame flattened the pans, so it is off by
default) — held on its last frame,
captioned (rendered by Playwright, overlaid), and joined by hard cuts through the concat
demuxer — one decoder at a time; a single xfade graph over every input was killed for
memory twice.

The cut is now **one connected flow on Northwind**, `scenes/supercut-northwind-flow.mjs`:
one recording, and after the opening load no page is reached by a goto. The Employees
view opens in Graph mode (its `ac:mode` is patched to `ac:GraphMode` off camera and put
back afterwards); an employee's node blooms with its own document; the Territory pill
pivots the graph to the 53 territories and the Region pill to the four regions; the
Southern node's link opens the region's page. Everything else happens on that page: a
city through the form and its pin on the region's own map (the view re-reads itself after
Save); Create ▸ SELECT, the query typed and saved; a bar chart from it; Content mode from
the document's mode toggle; a sentence; the chart embedded as an Object; the sentence
dragged below it. The whole take is 2× footage, graph included: `lib/graph.mjs` scales
the canvas-local points `graph2ScreenCoords` returns by the document's CSS zoom (the
canvas rect is in zoomed viewport pixels, the graph's own coordinates are not), so the
earlier zoom-1 exception for graph takes is gone. Southern's blocks and the
previous take's query and chart are cleared off camera first. Beats mark 17 shots; the
cut list picks the stretches, so what the viewer does not see (the form being filled at
3×, the query typed at 2×) is compressed, never cut to another page.

Earlier takes stay in `scenes/` for the record: `supercut-northwind-graph.mjs` (the order
graph, then the customer record's Related results hopped to the reps and on to their
territories, Rockville opened, a rep hired for it on the Person form its view constructs,
the grid re-read one face richer, the breadcrumb up to Sales territories — one take, so
the cut moves the camera between those beats and never cuts; recorded with
`SCHEME=dark`, like every take in the dark cut), `supercut-northwind.mjs` (gesture shots 4, 9, 15, 12, 8), `supercut-compose.mjs`
(the compose sequence on its own page — one take from the query to the drag, cut as one continuous camera: every shot continues the previous one, the long gestures at 2–3×), `supercut-unesco.mjs`, `supercut-ltlod.mjs`, and
the Rebrickable pair `supercut-rebrickable.mjs` / `supercut-rebrickable-flow.mjs` —
Rebrickable was dropped from the cut: its stack runs `atomgraph/linkeddatahub:5.6.0`,
which has no dark theme, so its shots came out light in a dark cut.

The takes are 2× footage: a 2880×1800 viewport with the document zoomed 200 %
(`lib/supercut.mjs`), since the video is the viewport in CSS pixels. `FLAGS=--shots=6,9`
records a subset of a gesture take; **rename the take to its own track straight after**
(`supercut-rebrickable-b`), then point the JSON's shots at it — a second `make scene` of
the same file overwrites `tracks/<scene>.webm`, which is how one good take was lost.

Stacks: `linkeddatahub.com` serves Northwind and UNESCO; Rebrickable is `:4444`; LTLOD is
`:4443`. Each stack's nginx also binds a client-certificate port, and the defaults collide
— LTLOD's 5443 with `linkeddatahub.com`, Rebrickable's 5444 with the LinkedDataHub dev
stack — so start them with a free one: `HTTPS_CLIENT_CERT_PORT=5445 docker compose up -d`
in `../LTLOD`, `HTTPS_CLIENT_CERT_PORT=5446 docker compose up -d nginx` in `../Rebrickable`
(the port is only nginx's; the rest of the stack is unaffected). `docker compose stop` /
`docker compose start` put everything back.

A page scroll on camera is `easeScrollTo` (`lib/supercut.mjs`): the page glides to the
element over ~1.4 s on an ease-in-out, so a static crop reads as a slow pan. The
browser's own smooth scroll is a 300 ms lurch, and a pan in the cutter over a page that
jumps underneath cannot be smooth either.

Drag-and-drop on camera is the app's own drag events (`dragBlock` in `lib/editing.mjs`):
Playwright's pointer drags and `dragTo` never reach the handler, a dispatched `DragEvent`
whose `DataTransfer` carries `application/vnd.atomgraph.linkeddatahub.block` does, and the
pointer overlay travels alongside. The app moves the dragged block **after** the drop
target (`client/block.xsl:612`), so a drop on the previous sibling is a no-op — to put the
chart above the sentence, the sentence is dragged onto the chart.

Not in the cut, and why:

- **File drop** — the content body accepts only the app's block type (`client/block.xsl:698`);
the set's edit form on Rebrickable offers no file input either (`foaf:depiction` is a URL).
- **Map popup on LTLOD** — the pixel-scan pin click did not open a popup at 2×.
- **Search on Rebrickable** — the dialog answered "the endpoint could not be reached" on
a 158 k-document search; the lookup-by-title combobox also times out there, so the
grid embed pastes a copied URI instead.
- **Tree expand on UNESCO** — the branch did not open (4 rows before and after).
- **Versioning, ACL, import wizard** — not shot: no history on these stacks; the other two
were not designed.

## Vector mock (retired)

Retired 2026-09-18: too much of every shot was imitation and the result read as fake beside
the recorded cut. Left in place, not extended. The shots were not screen recordings. Each shot is a page under `mock/`
(`form-pin.html`, `bloom.html`, `shapes.html`) that composes the app's **own components** —
markup and stylesheets captured from the running dataspace (`scenes/capture-states.mjs`
writes `mock/states/app.css` with every `@import` and `url()` inlined, plus the modal, the
view block head, the results in each mode, the graph chrome as `mock/templates/*.html`) —
around **made-up data**, and animates between states on a `window.render(t)` timeline.
`render/mock.mjs` serves the page, steps `t` frame by frame, screenshots at 2× and encodes:
crisp at any zoom, no cursor, no chrome the shot does not want, and the timing is the
shot's, not the app's. Maps, graphs and charts are drawn (SVG over a captured basemap;
d3-force for layout, in the app's hue-per-type idiom) because their canvases do not
serialise. The contract: every **state** shown is one the product produces; the **motion**
between states is invented; the **data** is whatever reads best. `render/mock-cut.json` +
`make supercut SUPERCUT=render/mock-cut.json` joins the shots.
Loading
Loading