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
4 changes: 3 additions & 1 deletion codex_procedures/content.generate/PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ Write exactly two UTF-8 Markdown files at the declared `output.path` and
`output.companion_path`. For `draft`, the primary file is public copy only: start with a `# Title`,
then the complete article/page draft with factual sources beside the supported claims.
Shape it with the search-and-answer-engines skill: answer first, question headings, a table
when comparing options, an FAQ where it fits and `## Sources` last.
when comparing options, an FAQ where it fits and `## Sources` last. For an article, use the
page-figures skill for any figure, diagram, interactive piece, video or callout it needs; an
answer page or a page refresh has none.
No Tin frontmatter, checklists, generation notes or approval commentary belongs in this file.
For any no-draft outcome, write a short assessment instead of an article. Its exact format is
`# Content assessment`, a blank line, the exact `rationale` string from the judgment below,
Expand Down
77 changes: 77 additions & 0 deletions codex_procedures/content.generate/skills/page-figures/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: page-figures
description: Add figures, diagrams, interactive pieces, videos and callouts to an article draft only where they show what the words can't, as files in its assets folder that the founder approves with the article.
---

# Figures, diagrams, interactive pieces and videos

Add one only where it shows something the words can't carry as well: a process, a comparison,
a structure, a measured result. Most articles need none, one or two. Never decorate.

Your output instructions name the article's assets folder. Write each file directly in it and
refer to it from the article with a path that starts `./<folder name>/`. Tin keeps only the files
the article refers to, and the founder approves them with the words.

## Diagram

A `mermaid` block for a flow, a sequence, a hierarchy or a timeline. Tin draws it in the
project's brand colours, so don't set colours or styles in it. Keep it to the few nodes the point
needs.

## Figure

An SVG file in the assets folder:

```markdown
![What the figure shows](./<folder name>/<file>.svg "What the reader should notice")
```

The alt text says what the figure shows; the caption says what to notice. Draw it plainly, with
real labels and the article's own numbers. The SVG must have no script, no event handlers, no
`foreignObject` and no links or images from elsewhere; Tin leaves out an SVG that has any.

## Interactive piece

One self-contained HTML file in the assets folder, only when interaction teaches something a still
figure can't, such as a simulation or a calculator:

````markdown
```tin-embed
src: ./<folder name>/<file>.html
height: 420
title: What the reader can do with it
```
````

Inline its CSS and JavaScript. It runs with no network access: no requests, no outside fonts,
libraries or images. Keep it small, give it a sensible default state, and make it work with a
mouse, a keyboard and a touch screen.

## Video

A video that already exists at a public address, on the founder's site or on YouTube, Vimeo, Loom
or Mux:

````markdown
```tin-video
url: https://www.youtube.com/watch?v=...
title: What it shows
```
````

Never invent a video or an address. A long video file is linked, never copied into the folder.

## Callout

`> [!NOTE]`, `> [!TIP]` or `> [!WARNING]` on the first line of a quote, for a short aside the
reader shouldn't miss. Use one or two at most.

## Existing pages

When the brief points to an existing page that has figures and you can read its source, keep
them: copy an inline SVG into the folder as its own file, or rebuild an interactive piece as an
embed with its script and data. Don't describe a figure in words instead of keeping it. If you
can't carry one over, say which and why in Generation notes.

List every figure, diagram, interactive piece and video in Generation notes with one line on what
each shows.
8 changes: 5 additions & 3 deletions codex_procedures/content.public_article/PROMPT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ Plan before drafting, but publish only the finished article in the declared Mark
article should open in the reader's world, land one salient idea, explain the mechanism, use concrete
evidence, acknowledge material assumptions and limitations, and leave the reader with a useful
contribution. Shape it with the search-and-answer-engines skill: its search-listing frontmatter
comes first, then the `# ` title and an answer-first opening. Apply the public-article-edit pass
before finishing.
comes first, then the `# ` title and an answer-first opening. Use the page-figures skill for any
figure, diagram, interactive piece, video or callout the article needs. Apply the public-article-edit
pass before finishing.

This is a reviewable draft, not a publishing action. Do not change another project file, publish to
This is a reviewable draft, not a publishing action. Do not change another project file (the
article's assets folder is part of the draft), publish to
a website, send the article, contact anyone, or ask an interactive question. Put unresolved choices
or missing evidence into the separate Generation notes document, never the public copy.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: page-figures
description: Add figures, diagrams, interactive pieces, videos and callouts to an article draft only where they show what the words can't, as files in its assets folder that the founder approves with the article.
---

# Figures, diagrams, interactive pieces and videos

Add one only where it shows something the words can't carry as well: a process, a comparison,
a structure, a measured result. Most articles need none, one or two. Never decorate.

Your output instructions name the article's assets folder. Write each file directly in it and
refer to it from the article with a path that starts `./<folder name>/`. Tin keeps only the files
the article refers to, and the founder approves them with the words.

## Diagram

A `mermaid` block for a flow, a sequence, a hierarchy or a timeline. Tin draws it in the
project's brand colours, so don't set colours or styles in it. Keep it to the few nodes the point
needs.

## Figure

An SVG file in the assets folder:

```markdown
![What the figure shows](./<folder name>/<file>.svg "What the reader should notice")
```

The alt text says what the figure shows; the caption says what to notice. Draw it plainly, with
real labels and the article's own numbers. The SVG must have no script, no event handlers, no
`foreignObject` and no links or images from elsewhere; Tin leaves out an SVG that has any.

## Interactive piece

One self-contained HTML file in the assets folder, only when interaction teaches something a still
figure can't, such as a simulation or a calculator:

````markdown
```tin-embed
src: ./<folder name>/<file>.html
height: 420
title: What the reader can do with it
```
````

Inline its CSS and JavaScript. It runs with no network access: no requests, no outside fonts,
libraries or images. Keep it small, give it a sensible default state, and make it work with a
mouse, a keyboard and a touch screen.

## Video

A video that already exists at a public address, on the founder's site or on YouTube, Vimeo, Loom
or Mux:

````markdown
```tin-video
url: https://www.youtube.com/watch?v=...
title: What it shows
```
````

Never invent a video or an address. A long video file is linked, never copied into the folder.

## Callout

`> [!NOTE]`, `> [!TIP]` or `> [!WARNING]` on the first line of a quote, for a short aside the
reader shouldn't miss. Use one or two at most.

## Existing pages

When the brief points to an existing page that has figures and you can read its source, keep
them: copy an inline SVG into the folder as its own file, or rebuild an interactive piece as an
embed with its script and data. Don't describe a figure in words instead of keeping it. If you
can't carry one over, say which and why in Generation notes.

List every figure, diagram, interactive piece and video in Generation notes with one line on what
each shows.
6 changes: 6 additions & 0 deletions docs/content-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ the next roadmap item.
separate artifact and viewer action, never text or a link embedded in public copy.
- Approval is bound to the current reviewed revision. An old approval cannot approve
a newer draft. Only the final approved copy is eligible for configured delivery.
- An article (content.public_article 1.9.0, content.generate 1.10.0 articles) may carry
figures, diagrams, interactive pieces, videos and callouts. The reader shows them as
they'll ship: SVG figures as images, embeds in a sandboxed frame with no network or
access to Tin, videos from YouTube, Vimeo, Loom or Mux (a video file loads only on
play), and a note under the title for any file Tin left out. The Decisions card says
what the draft carries, for example "With 2 figures and 1 interactive piece."
- A failed revision needs explicit retry. Feedback is not discarded by generic retry,
and a stopped revision closes its waiting review ancestry.
- Discard turns down anything waiting in Decisions: the run ends as declined, nothing it
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"type": "module",
"scripts": {
"test:browser": "node --test web/raw-file-security.browser.test.js web/fonts.browser.test.js web/diagram-renderer.browser.test.js web/diagram-viewport.browser.test.js scripts/check_diagram.test.mjs web/json-viewer.test.js web/json-viewer.browser.test.js web/output-comparison.browser.test.js web/content-plan.browser.test.js web/content-plan-page.browser.test.js web/billing.browser.test.js web/run-payment-card.browser.test.js web/style-capture.browser.test.js web/content-draft.browser.test.js web/content-delivery.browser.test.js web/workflow-review.browser.test.js web/auth.browser.test.js web/mcp-consent.browser.test.js web/app-domain.browser.test.js web/lock-page.browser.test.js web/project-menu.browser.test.js web/chat.browser.test.js web/project-connections.browser.test.js web/code-workflow.browser.test.js web/page-url.browser.test.js web/files-page.browser.test.js web/narrow-layout.browser.test.js web/decisions.browser.test.js web/website-changes.browser.test.js web/integration-resource.browser.test.js web/x-posts.browser.test.js web/x-feedback.browser.test.js web/x-reader-page.browser.test.js web/x-integration.browser.test.js web/x-workflow-setup.browser.test.js web/monthly-schedule.browser.test.js",
"test:browser": "node --test web/raw-file-security.browser.test.js web/fonts.browser.test.js web/diagram-renderer.browser.test.js web/diagram-viewport.browser.test.js scripts/check_diagram.test.mjs web/json-viewer.test.js web/json-viewer.browser.test.js web/output-comparison.browser.test.js web/content-plan.browser.test.js web/content-plan-page.browser.test.js web/billing.browser.test.js web/run-payment-card.browser.test.js web/style-capture.browser.test.js web/content-draft.browser.test.js web/content-delivery.browser.test.js web/workflow-review.browser.test.js web/auth.browser.test.js web/mcp-consent.browser.test.js web/app-domain.browser.test.js web/lock-page.browser.test.js web/project-menu.browser.test.js web/chat.browser.test.js web/project-connections.browser.test.js web/code-workflow.browser.test.js web/page-url.browser.test.js web/files-page.browser.test.js web/narrow-layout.browser.test.js web/decisions.browser.test.js web/website-changes.browser.test.js web/integration-resource.browser.test.js web/x-posts.browser.test.js web/x-feedback.browser.test.js web/x-reader-page.browser.test.js web/x-integration.browser.test.js web/x-workflow-setup.browser.test.js web/monthly-schedule.browser.test.js web/page-bundles.browser.test.js",
"test:x-browser": "node --test web/x-posts.browser.test.js web/x-feedback.browser.test.js web/x-reader-page.browser.test.js web/x-integration.browser.test.js web/x-workflow-setup.browser.test.js web/monthly-schedule.browser.test.js",
"test:files-browser": "node --test web/files-page.browser.test.js",
"test:narrow-layout-browser": "node --test web/narrow-layout.browser.test.js",
Expand Down
9 changes: 8 additions & 1 deletion src/tin_lite/activities.py
Original file line number Diff line number Diff line change
Expand Up @@ -4447,6 +4447,13 @@ async def request_codex_procedure_review(self, run_id_text: str) -> bool:
repo_id=project.state_repo_id, commit_sha=sha, path=path
)
artifact_title, lede = display_title(raw), review_line(raw)
from tin_lite.page_assets import attachments_line

attachments = attachments_line(
canonical.result.get("checkpoint"), raw.decode("utf-8", errors="replace")
)
else:
attachments = None
destination = (
f"Approval opens an unmerged GitHub PR in {delivery['repository']}"
+ ("." if delivery.get("system_run_id") else f" at {delivery['path']}.")
Expand All @@ -4469,7 +4476,7 @@ async def request_codex_procedure_review(self, run_id_text: str) -> bool:
artifact_path=path,
artifact_title=artifact_title,
summary=summary,
explanation=" ".join(part for part in (lede, destination) if part),
explanation=" ".join(part for part in (lede, attachments, destination) if part),
)
if required:
from tin_lite.organic_content import project_review_progress
Expand Down
73 changes: 70 additions & 3 deletions src/tin_lite/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,13 @@ class MarkdownHeadingView(BaseModel):
title: str


class MarkdownAssetView(BaseModel):
"""A figure or embed the reader loads from the project at the document's revision."""

path: str
media_type: str


class MarkdownDocumentView(BaseModel):
markdown: str
html: str
Expand All @@ -1135,6 +1142,57 @@ class MarkdownDocumentView(BaseModel):
size_bytes: int | None = None
related_documents: list[dict[str, str]] = Field(default_factory=list)
sha256: str | None = None
assets: list[MarkdownAssetView] = Field(default_factory=list)
# Files the draft referred to that Tin left out, each with its reason.
asset_notes: list[str] = Field(default_factory=list)


async def _run_document_assets(database, run, path, source):
"""A draft's kept figures and embeds, from its published checkpoint, and what was left out."""
from tin_lite import page_assets

if source != "canonical" or not path.endswith(".md") or run.executor != "codex.procedure":
return [], [], None
published = await database.get_effect(f"{run.id}:procedure_canonical_commit")
checkpoint = (
(published.result or {}).get("checkpoint")
if published is not None and published.status == "completed"
else None
)
persisted = await database.get_effect(f"{run.id}:procedure_artifact_persist")
dropped = ((persisted.result or {}) if persisted is not None else {}).get("dropped_assets")
notes = [
f"{item['path'].rsplit('/', 1)[-1]} was left out: {item['reason']}."
for item in dropped or []
]
assets = [
MarkdownAssetView(path=item["artifact_path"], media_type=item["media_type"])
for item in (checkpoint or {}).get("assets") or []
]
return assets, notes, page_assets.folder(path) if assets or notes else None


async def _file_document_assets(storage, project, path, revision, markdown):
"""The figures and embeds a Markdown file in the project refers to that exist there."""
from tin_lite import page_assets

if not path.endswith(".md"):
return [], None
referenced = page_assets.referenced(markdown, path)
if not referenced:
return [], None
try:
present = set(
await storage.list_canonical_files_at(repo_id=project.state_repo_id, revision=revision)
)
except LookupError:
return [], None
assets = [
MarkdownAssetView(path=item, media_type=page_assets.media_type(item))
for item in referenced
if item in present
]
return assets, page_assets.folder(path)


class ProjectFileView(BaseModel):
Expand Down Expand Up @@ -2721,7 +2779,7 @@ async def get_project_file_document(
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
detail="project file is not Markdown",
)
_, content = await _read_project_file(
project, content = await _read_project_file(
project_id=project_id,
path=path,
revision=revision,
Expand All @@ -2735,7 +2793,10 @@ async def get_project_file_document(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="Markdown project file is not UTF-8",
) from exc
document = render_markdown(markdown)
assets, folder = await _file_document_assets(
request.app.state.runtime.storage, project, path, revision, markdown
)
document = render_markdown(markdown, asset_folder=folder)
source_query = f"path={quote(path, safe='')}&revision={revision}"
response.headers["X-Tin-File-Source"] = "code.storage"
response.headers["X-Tin-File-Revision"] = revision
Expand All @@ -2753,6 +2814,7 @@ async def get_project_file_document(
path=path,
revision=revision,
size_bytes=len(content),
assets=assets,
)


Expand Down Expand Up @@ -4469,7 +4531,10 @@ async def get_artifact_document(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="Markdown artifact is not UTF-8",
) from exc
document = render_markdown(markdown)
assets, asset_notes, folder = await _run_document_assets(
request.app.state.runtime.database, run, output.path, source
)
document = render_markdown(markdown, asset_folder=folder)
response.headers["X-Tin-Artifact-Source"] = "code.storage"
response.headers["X-Tin-Output-Kind"] = source
response.headers["X-Tin-Output-Revision"] = output.revision
Expand All @@ -4491,6 +4556,8 @@ async def get_artifact_document(
headings=[
MarkdownHeadingView(id=heading.id, title=heading.title) for heading in document.headings
],
assets=assets,
asset_notes=asset_notes,
)


Expand Down
9 changes: 7 additions & 2 deletions src/tin_lite/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
from tin_lite.model_providers import ModelCapability, ModelRoute, ProviderName
from tin_lite.organic_audit import AUDIT_KEY, AUDIT_POLICY, MARKETS
from tin_lite.organic_audit_ai import AI_CONTRACT, AI_SCHEMAS
from tin_lite.page_assets import AssetPolicy
from tin_lite.procedure_documents import AGENT_REVISION
from tin_lite.procedures import (
BROWSER_SANDBOX_PROFILE,
Expand Down Expand Up @@ -852,7 +853,7 @@ def definition_and_files_with_wiki(
"route you chose, and a refresh changes exactly the approved lines. The roadmap stays "
"unchanged.",
executor=CODEX_PROCEDURE_EXECUTOR,
version_label="1.9.0",
version_label="1.10.0",
system=ORGANIC_TRAFFIC_SYSTEM,
# A weekly occurrence drafts the next article in plan order and holds while an
# earlier draft from the same program still waits for review.
Expand Down Expand Up @@ -925,6 +926,8 @@ def definition_and_files_with_wiki(
output_path_template=content_draft.PATH_TEMPLATE,
output_validator=content_draft.EDITORIAL_VALIDATOR,
output_max_bytes=80_000,
# An article's figures and embeds (page_assets), approved with its words.
output_assets=AssetPolicy(max_files=12, max_bytes=2_000_000),
project_skills=(
ProjectSkillDependency(name="writing-style", path=STYLE_PATH, required=False),
),
Expand Down Expand Up @@ -1776,7 +1779,7 @@ def definition_and_files_with_wiki(
"public article."
),
executor=CODEX_PROCEDURE_EXECUTOR,
version_label="1.8.0",
version_label="1.9.0",
system=ORGANIC_TRAFFIC_SYSTEM,
prerequisites=(
WorkflowPrerequisite(
Expand Down Expand Up @@ -1849,6 +1852,8 @@ def definition_and_files_with_wiki(
output_path_template=article_review.PATH_TEMPLATE,
output_validator="public-article.v2",
output_max_bytes=300_000,
# Its figures and embeds (page_assets), approved with its words.
output_assets=AssetPolicy(max_files=12, max_bytes=2_000_000),
project_skills=(
ProjectSkillDependency(
name="writing-style",
Expand Down
Loading
Loading