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
7 changes: 4 additions & 3 deletions .claude/skills/explore/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,10 @@ one (exploration 0421).
`two-way` means just do it.

Past its `review` date a doc is listed in `docs/explorations/STALE.md` by
`pnpm check:exploration-fallow`. Being listed is not a failure; it means
the claim on future attention has lapsed. Renew it (`review: <later>`) or
release it (`status: withdrawn`) — both are one-line frontmatter edits.
`pnpm check:exploration-fallow`. With no date, the default window is 90 days.
Stale is an informational age classification, not a CI failure or a requirement
to renew, withdraw, or implement the draft. Leave dates and status unchanged
unless an actual decision about the work calls for an update.

> [!IMPORTANT]
> Never signal any of this by renaming the file. Status in the filename
Expand Down
13 changes: 6 additions & 7 deletions .claude/skills/mvp-followup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,12 @@ something genuinely blocks the current work from being used.

## Choosing among stale explorations

`docs/explorations/STALE.md` lists every document past its `review:` date, and
opens with the backlog's **retirement curve** (exploration 0430). Read the curve
before the list: it does not fall, so an old `[_]` is not work in progress — it
is a decision already made by inaction. Withdrawing one (`status: withdrawn`) is
a legitimate closeout and often the honest answer; renewing it (`review:`) is the
other. Neither renames the file. Proposing to _start_ a 120-day-old document is
new scope, not follow-up.
`docs/explorations/STALE.md` reports drafts past their `review:` date or the
default 90-day window, alongside age-cohort statistics (exploration 0430).
Staleness is informational: age alone does not require action, fail CI, or mean
the work has been rejected. Do not renew dates or withdraw drafts merely to
clear the report. Choose follow-up by its relevance to the current task;
starting an unrelated old draft is new scope.

## Output shape

Expand Down
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ jobs:
steps:
# fetch-depth: 0 — check:exploration-fallow dates each exploration from
# the commit that introduced it. A shallow clone has no such commit, so
# every document would look new and the ratchet would pass vacuously; it
# fails loudly below a coverage floor rather than reporting a false green.
# draft ages would be unreadable. Keep enough history for an honest report;
# staleness itself is informational and never a merge gate.
- uses: actions/checkout@v4
with:
fetch-depth: 0
Expand Down Expand Up @@ -117,13 +117,13 @@ jobs:
- name: Exploration references resolve (0421)
run: pnpm check:exploration-links

# The backlog grows ~85 documents a month and nothing closes one. Ratchet
# only — the count may not EXCEED the committed baseline, so a green repo
# stays green and the fix is always a one-line frontmatter edit. Needs the
# full history checked out below: creation dates are what make a document
# "stale", and a shallow clone has none. (0421.)
- name: Exploration backlog ratchet (0421)
run: pnpm check:exploration-fallow
# Draft age is a derived status, not a deadline to renew or close work.
# Publish the current report for readers of the job summary; no stale-count
# threshold applies. Generation errors still surface as errors. (0421.)
- name: Exploration age report (0421)
run: |
pnpm check:exploration-fallow
cat docs/explorations/STALE.md >> "$GITHUB_STEP_SUMMARY"

- name: Publish closure (0220)
run: pnpm check:publish-closure
Expand Down
3 changes: 0 additions & 3 deletions docs/explorations/.fallow-baseline.json

This file was deleted.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

238 changes: 116 additions & 122 deletions docs/explorations/STALE.md

Large diffs are not rendered by default.

106 changes: 20 additions & 86 deletions scripts/check-exploration-fallow.mjs
Original file line number Diff line number Diff line change
@@ -1,54 +1,31 @@
#!/usr/bin/env node
/**
* Ratchet the exploration backlog (exploration 0421).
* Report exploration age (exploration 0421).
*
* xNet's build loop is already fast — median PR cycle time under an hour, CI at
* eight minutes. What has no clock at all is *deciding what to build*: 259
* explorations sit at `[_]`, the backlog grows ~85 documents a month, and
* nothing ever closes one. A document written in February and never started is
* indistinguishable from one written yesterday.
* Drafts become stale after their explicit review date, or 90 days after
* first appearing in git. Age is informational: stale drafts can remain open
* without blocking CI or requiring a new date. This report never rewrites,
* renames, or withdraws the source documents.
*
* This gives the backlog the two things every fast project on Collison's list
* had and this one lacks: a decider and an expiry.
*
* review: <YYYY-MM-DD> when to RE-DECIDE — not when to ship
* decider: <name> who closes it; a single name, never a list
*
* Absent `review:`, a document is due 90 days after it first appeared. 90 is
* measured, not guessed: it marks 41 of 276 undecided documents stale (15%),
* where 180 marks *zero* today and ~200 in three months as the June/July bulge
* crosses at once. A gate that cannot fire is not lenient, it is absent.
*
* Expiry never moves, renames or deletes anything. Status in the *filename* is
* a proven link-rot generator (see check-exploration-links.mjs); `review:` and
* `status: withdrawn` live in frontmatter precisely because changing them
* renames nothing. Withdrawing is a legitimate, encouraged outcome — recording
* that a decision was *made* is the point.
*
* Named consumer: `docs/explorations/STALE.md`, which `/mvp-followup` reads to
* answer "what's next" — today it has no principled way to choose among 259
* identical-looking candidates.
*
* Pass condition: the stale count must not EXCEED the committed baseline in
* `.fallow-baseline.json`. A ratchet, never an absolute — per AGENTS.md, and
* per fallow.yml's own postmortem, where gating the absolute made every Monday
* a guaranteed red nobody consumed.
* Consumers: `docs/explorations/STALE.md`, `/mvp-followup`, and the CI job
* summary. Generation succeeds when the report can be read and written;
* the number of stale drafts does not affect the exit status.
*
* Run: `node scripts/check-exploration-fallow.mjs` (or `pnpm check:exploration-fallow`).
* `--write-baseline` reseeds the baseline to today's count.
*/
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs'
import { readFileSync, writeFileSync, readdirSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { join, resolve } from 'node:path'
import { dayDiff, dueDay, formatDay, isOverdue, overdueDays } from './exploration-fallow/dates.mjs'

const root = resolve(process.cwd())
const DIR = join(root, 'docs/explorations')
const BASELINE = join(DIR, '.fallow-baseline.json')
const STALE_INDEX = join(DIR, 'STALE.md')
const DEFAULT_WINDOW_DAYS = 90

const writeBaseline = process.argv.includes('--write-baseline')
if (process.argv.includes('--write-baseline')) {
throw new Error('Exploration age is report-only; --write-baseline is no longer supported.')
}

/**
* Git hooks export GIT_DIR / GIT_WORK_TREE, which hijack any `git` subprocess
Expand Down Expand Up @@ -306,17 +283,9 @@ const index = [
'',
'# Stale explorations',
'',
'Explorations past their `review:` date (or 90 days old with none). Being',
'listed here is not a failure — it means the claim this document makes on',
'future attention has lapsed and needs renewing or releasing.',
'',
'Two fixes, both one-line frontmatter edits. Neither renames the file, so',
'neither breaks an inbound reference:',
'',
'```yaml',
'review: 2027-02-01 # renew the claim',
'status: withdrawn # release it; the document stays exactly where it is',
'```',
'Drafts are classified as stale after their `review:` date, or after the',
'default 90-day window. This is an age report, not a CI failure or a deadline',
'to build, renew, or close anything. Stale drafts can remain open until useful.',
'',
`**${stale.length}** stale of ${considered} undecided.`,
'',
Expand All @@ -334,11 +303,9 @@ const index = [
'',
...(shape
? [
`The curve does not fall: ${shape.first}% of documents at least a day old are`,
`unshipped, and ${shape.last}% at ${shape.lastDay} days. An exploration is checked off`,
'within days of being written, or never — so an old `[_]` is not a pending',
'decision, it is a decision already made by inaction. Renew it deliberately,',
'or withdraw it; both are one line and neither renames the file.',
`${shape.first}% of documents at least a day old remain unshipped,`,
`compared with ${shape.last}% of those at least ${shape.lastDay} days old.`,
'These are age cohorts, not requirements to retire drafts.',
''
]
: []),
Expand Down Expand Up @@ -368,38 +335,5 @@ const index = [

writeFileSync(STALE_INDEX, index)

if (writeBaseline) {
writeFileSync(BASELINE, `${JSON.stringify({ count: stale.length }, null, 2)}\n`)
console.log(`✓ baseline seeded at ${stale.length}`)
process.exit(0)
}

const baseline = existsSync(BASELINE) ? JSON.parse(readFileSync(BASELINE, 'utf8')).count : 0

// Printed on green runs too: the number should be familiar long before it is
// ever binding (0283 — a gate whose first appearance is a failure gets ignored).
console.log(`explorations past review date: ${stale.length} (baseline ${baseline})`)

if (stale.length > baseline) {
// Least-overdue first: whatever just crossed the line is what this change
// most likely introduced, and it is buried if the list is alphabetical.
const newest = [...stale].sort((a, b) => a.days - b.days)
console.error(
`\n✗ stale explorations increased: ${stale.length} > ${baseline}\n\n` +
' Most recently gone stale — start here:\n' +
newest
.slice(0, 10)
.map((s) => ` ${s.file} (${s.days}d overdue${s.decider ? `, ${s.decider}` : ''})`)
.join('\n') +
(stale.length > 10 ? `\n … and ${stale.length - 10} more` : '') +
'\n\n Both fixes are one-line frontmatter edits — no rename, so no\n' +
' inbound reference breaks:\n' +
' review: 2027-02-01 # renew the claim\n' +
' status: withdrawn # release it; the document stays put\n\n' +
` Full list: docs/explorations/STALE.md\n`
)
process.exit(1)
}

console.log(`✓ exploration backlog OK (${considered} undecided, ${stale.length} stale)`)
process.exit(0)
console.log(`Exploration age report: ${stale.length} stale of ${considered} undecided.`)
console.log('Staleness is informational and does not block CI. See docs/explorations/STALE.md.')
2 changes: 1 addition & 1 deletion scripts/exploration-fallow/dates.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Date arithmetic for the exploration fallow ratchet.
* Date arithmetic for the exploration age report.
*
* Split out of `check-exploration-fallow.mjs` so it can be tested without
* shelling out to git or rewriting `STALE.md`.
Expand Down
18 changes: 18 additions & 0 deletions site/src/components/marketing/Closing.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
const {
title = 'Try the xNet alpha',
body = 'Download the desktop app or open the browser demo.'
} = Astro.props
---

<section class="m-cta">
<div class="m-wrap">
<div>
<h2 class="m-heading">{title}</h2>
<p class="m-copy mt-5">{body}</p>
</div>
<a href="/download" class="m-button">
Try xNet <span aria-hidden="true">↗</span>
</a>
</div>
</section>
17 changes: 17 additions & 0 deletions site/src/components/marketing/PageHero.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
interface Props {
eyebrow: string
title: string
description: string
}
const { eyebrow, title, description } = Astro.props
---

<header class="m-page-hero">
<div class="m-wrap">
<p class="m-eyebrow">{eyebrow}</p>
<h1 class="m-title">{title}</h1>
<p class="m-lede">{description}</p>
<slot />
</div>
</header>
85 changes: 85 additions & 0 deletions site/src/components/marketing/Substrate.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
import CodeBlock from '../ui/CodeBlock.astro'
const pieces = [
{
name: 'Nodes',
label: 'Reusable records',
example: 'A task, a contact, a page.',
description: 'Each record has an ID. Relations connect records across your workspace.',
color: 'text-indigo-600 dark:text-indigo-400 bg-indigo-500/10'
},
{
name: 'Schemas',
label: 'A shared structure',
example: 'Title, status, project.',
description: 'Define fields and relations. Add fields in your namespace or create a new type.',
color: 'text-emerald-700 dark:text-emerald-400 bg-emerald-500/10'
},
{
name: 'Views',
label: 'Different ways to work',
example: 'A table, a board, your own app.',
description: 'Read and edit the same records through a different interface.',
color: 'text-violet-600 dark:text-violet-400 bg-violet-500/10'
}
]
const schemaCode = `<span class="tok-keyword">import</span> { defineSchema, text, checkbox }
<span class="tok-keyword">from</span> <span class="tok-string">'@xnetjs/data'</span>

<span class="tok-keyword">const</span> Task = <span class="tok-function">defineSchema</span>({
name: <span class="tok-string">'Task'</span>,
namespace: <span class="tok-string">'xnet://my-app/'</span>,
properties: {
title: <span class="tok-function">text</span>({ required: <span class="tok-number">true</span> }),
done: <span class="tok-function">checkbox</span>({ default: <span class="tok-number">false</span> })
}
})`
---

<div class="mt-7 grid items-start gap-6 lg:grid-cols-2">
<ol class="grid gap-3" aria-label="Three parts of the xNet substrate">
{pieces.map((piece) => (
<li class="rounded-xl border border-border p-4">
<div class="flex flex-wrap items-center gap-3">
<h3 class={`rounded-md px-2 py-1 text-sm font-semibold ${piece.color}`}>{piece.name}</h3>
<span class="text-sm font-medium">{piece.label}</span>
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<strong class="font-medium text-gray-800 dark:text-gray-200">{piece.example}</strong>{' '}
{piece.description}
</p>
</li>
))}
</ol>
<div class="min-w-0">
<CodeBlock filename="task.schema.ts" code={schemaCode} />
<p class="mt-3 text-xs text-gray-500">
A schema defines the data. Your interface decides how to display it.
</p>
</div>
</div>
<details class="mechanics mt-5 rounded-xl border border-border bg-surface/40 px-5 py-3">
<summary class="cursor-pointer text-sm font-medium">
How storage, sync, and permissions work
</summary>
<div class="mt-4 grid gap-4 text-sm text-gray-600 dark:text-gray-400 sm:grid-cols-3">
<p>
<strong class="text-gray-900 dark:text-gray-100">Local storage.</strong> The workspace keeps a
copy on your device. Offline changes sync when connected.
</p>
<p>
<strong class="text-gray-900 dark:text-gray-100">Signed changes.</strong> Structured edits
form a signed, hash-linked log. Peers can check who signed an edit and detect changes to it.
</p>
<p>
<strong class="text-gray-900 dark:text-gray-100">Merge rules.</strong> Yjs merges document
edits; structured fields use last-writer-wins rules. Grants govern sharing and access.
</p>
</div>
<a
href="/docs/concepts/data-model/"
class="mt-4 inline-block text-sm text-indigo-600 hover:underline dark:text-indigo-400"
>
Data model documentation →
</a>
</details>
24 changes: 24 additions & 0 deletions site/src/components/marketing/WorkspaceImage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<figure>
<div class="m-screen">
<img
class="screen-light"
src="/images/workbench-light.png"
width="1600"
height="1000"
alt="xNet’s workspace with a document open beside the explorer. The document contains example project content."
loading="lazy"
/>
<img
class="screen-dark"
src="/images/workbench-dark.png"
width="1600"
height="1000"
alt="xNet’s workspace in dark mode, showing an example project document and explorer."
loading="lazy"
/>
</div>
<figcaption class="m-caption">
<span>The xNet workspace · example content</span>
<span>Documents, databases, and canvases on your device.</span>
</figcaption>
</figure>
Loading
Loading