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
39 changes: 39 additions & 0 deletions .github/workflows/production-lifecycle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Production lifecycle projection

# The local browser contract runs in npm test. This small network check runs
# daily and when the projection contract changes. It fetches exact pinned
# canonical inputs, verifies their hashes, runs the pinned organization
# validator, and refuses committed-output drift.

on:
schedule:
- cron: '40 13 * * *'
workflow_dispatch:
pull_request:
paths:
- 'production-lifecycle-source.json'
- 'public/production-lifecycle.json'
- 'public/schemas/production-lifecycle-public.schema.json'
- 'public/production-lifecycle.js'
- 'scripts/render_production_lifecycle.py'
- 'tests/productionLifecycle.test.js'
- '.github/workflows/production-lifecycle.yml'

permissions:
contents: read

concurrency:
group: production-lifecycle-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Verify the canonical projection
run: python -S scripts/render_production_lifecycle.py --check
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# openadapt-web

**Lifecycle: Beta**
**Lifecycle: Support**

Source for the [openadapt.ai](https://openadapt.ai) marketing site.

Expand All @@ -19,10 +19,10 @@ product documentation lives at [docs.openadapt.ai](https://docs.openadapt.ai).
## Technical status data

`public/status.json` is the canonical, machine-readable source of truth for
substrate lifecycle evidence and component versions, and
`tests/statusManifest.test.js` guards it. The marketing homepage deliberately
does not render this temporary maturity ledger; technical consumers can read the
manifest directly.
released component versions and substrate capability evidence.
`public/production-lifecycle.json` is the separate projection of the canonical
signed admission ledger. It derives Production at read time. The current ledger
is empty, so all seven product targets are not actively admitted.

## Tech stack

Expand Down
12 changes: 3 additions & 9 deletions components/Pricing.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Link from 'next/link'
import TierSelector from '@components/TierSelector'

import { track, EVENTS } from 'utils/analytics'
import status from '../public/status.json'

const {
LAUNCH_MONTHLY_RUN_CAP,
Expand All @@ -16,15 +15,10 @@ const {
/*
* Public offer terms come from the same code-owned launch contract that
* validates Stripe. Checkout still remains unavailable unless the configured
* live Product and Price match that contract exactly. The hosted maturity label is
* read from the canonical status manifest (public/status.json) so it can never
* drift from the single source of truth.
* live Product and Price match that contract exactly. The badge describes the
* managed-browser offer. It does not assign a Production state.
*/

const hostedStatusLabel =
status.substrates.find((substrate) => substrate.name === 'Hosted Cloud')
?.public_label || 'Beta'

function Check() {
return (
<span
Expand Down Expand Up @@ -260,7 +254,7 @@ export default function Pricing({ hostedOffer = null }) {
className="inline-block rounded-full border border-hairline bg-ground px-3 py-1 font-mono text-[10px] font-medium uppercase tracking-[0.14em] text-ink-2"
data-testid="hosted-status-label"
>
{hostedStatusLabel}
Managed browser
</span>
{runCapLabel && (
<p
Expand Down
8 changes: 4 additions & 4 deletions components/ProductStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ export default function ProductStatus() {
uses the strongest evidence it provides.
</p>
<p className="mx-auto mt-3 max-w-3xl text-center text-sm leading-relaxed text-ink-2 md:text-base">
Browser runs in production today; desktop, RDP, and Citrix
run through customer-controlled qualification. Each surface
carries its own measured acceptance evidence, published per
surface in the{' '}
Browser supports the complete record, compile, replay, and
verify loop. Desktop, RDP, and Citrix use the same workflow
contract through customer-controlled qualification. Each
surface carries its own measured acceptance evidence in the{' '}
<a
href="https://docs.openadapt.ai/get-started/what-works-today/"
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion components/StructuredData.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const releasedDesktopSubstrates = status.substrates
.filter((substrate) =>
['Windows', 'macOS', 'Linux'].includes(substrate.name)
)
.filter((substrate) => substrate.public_label === 'Available')
.filter((substrate) => substrate.capability_state === 'released')
.map((substrate) => substrate.name)

export const softwareApplicationNode = {
Expand Down
4 changes: 2 additions & 2 deletions data/published-version-claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@
"source_of_truth": "public/status.json#/versions",
"release_source_of_truth": "public/status.json#/releases",
"verified_on": "2026-08-26",
"evidence": "Published package and GitHub release records on 2026-08-26: openadapt 1.15.1, openadapt-flow 1.33.0, openadapt-capture 1.2.2, openadapt-desktop 0.15.0.",
"evidence": "Published package and GitHub release records on 2026-08-26: openadapt 1.16.0, openadapt-flow 1.33.0, openadapt-capture 1.2.2, openadapt-desktop 0.15.0.",
"packages": {
"launcher": "openadapt",
"flow": "openadapt-flow",
"capture": "openadapt-capture",
"desktop": "openadapt-desktop"
},
"versions": {
"launcher": "1.15.1",
"launcher": "1.16.0",
"flow": "1.33.0",
"capture": "1.2.2",
"desktop": "0.15.0"
Expand Down
29 changes: 14 additions & 15 deletions data/workflowCatalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,16 @@

const FLOW_REPO = 'https://github.com/OpenAdaptAI/openadapt-flow'

// Canonical substrate availability labels, reconciled to public/status.json.
// Each released backend is available behind the same governed loop; its exact
// evidence scope and deployment boundary live in the status manifest. The
// catalog entries below all run through the browser substrate.
export const SUBSTRATE_MATURITY = {
browser: 'Available',
windows: 'Available',
macos: 'Available',
linux: 'Available',
rdp: 'Available',
citrix: 'Available',
// Canonical released-capability labels, reconciled to public/status.json.
// Production admission is a separate read-time decision. The catalog entries
// below all run through the browser substrate.
export const SUBSTRATE_AVAILABILITY = {
browser: 'Released',
windows: 'Released',
macos: 'Released',
linux: 'Released',
rdp: 'Released',
citrix: 'Released',
}

// Shared honesty envelope. Every catalog entry sets these explicitly so the
Expand All @@ -44,7 +43,7 @@ export const CATALOG = [
'official image sha256:0aa4d3d5…f19f3ab, MariaDB 11.8.',
substrate: 'browser',
substrateLabel: 'Browser (Playwright)',
maturity: SUBSTRATE_MATURITY.browser,
availability: SUBSTRATE_AVAILABILITY.browser,
task:
'Create exactly one fictional patient (name, DOB, sex, address, ' +
'phone, and a reserved example.invalid email) and save it.',
Expand Down Expand Up @@ -126,7 +125,7 @@ export const CATALOG = [
'frappe_docker c004361e…a8960.',
substrate: 'browser',
substrateLabel: 'Browser (Playwright)',
maturity: SUBSTRATE_MATURITY.browser,
availability: SUBSTRATE_AVAILABILITY.browser,
task:
'Create exactly one synthetic Loan Application — product ' +
'“OpenAdapt Synthetic Term Loan”, amount 125000, 18 repayment ' +
Expand Down Expand Up @@ -208,7 +207,7 @@ export const CATALOG = [
'sha256:d4bc881e…adf29d; distribution commit cd6220d1…d0caa5.',
substrate: 'browser',
substrateLabel: 'Browser (Playwright)',
maturity: SUBSTRATE_MATURITY.browser,
availability: SUBSTRATE_AVAILABILITY.browser,
// openIMIS is explicitly a reference/demo environment, not a benchmark.
evidenceKind: 'reference demonstration (not a trial matrix)',
task:
Expand Down Expand Up @@ -283,7 +282,7 @@ export const REQUIRED_ENTRY_FIELDS = [
'application',
'version',
'substrate',
'maturity',
'availability',
'task',
'recording',
'policy',
Expand Down
88 changes: 88 additions & 0 deletions lib/productionLifecycleEndpoint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
export const AUTHORITY_FETCH_TIMEOUT_MS = 4000

export function createServerFetch({
fetchImpl = globalThis.fetch,
timeoutMs = AUTHORITY_FETCH_TIMEOUT_MS,
githubToken = process.env.GITHUB_TOKEN,
} = {}) {
if (typeof fetchImpl !== 'function') {
throw new TypeError('fetchImpl must be a function')
}
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
throw new TypeError('timeoutMs must be a positive number')
}

return async function serverFetch(url, options = {}) {
const parsed = new URL(url)
const headers = new Headers(options.headers || {})
headers.set('User-Agent', 'OpenAdapt-Web/1.0 (https://openadapt.ai)')
if (parsed.hostname === 'api.github.com') {
headers.set('Accept', 'application/vnd.github+json')
if (githubToken) {
headers.set('Authorization', `Bearer ${githubToken}`)
}
}

const controller = new AbortController()
const fetchPromise = fetchImpl(url, {
...options,
headers,
signal: controller.signal,
})
let timeoutId
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
const error = new Error('Lifecycle authority fetch timed out')
controller.abort(error)
reject(error)
}, timeoutMs)
})
try {
return await Promise.race([fetchPromise, timeoutPromise])
} finally {
clearTimeout(timeoutId)
}
}
}

export function createSingleflightLoader(loadState) {
if (typeof loadState !== 'function') {
throw new TypeError('loadState must be a function')
}

let inFlight = null
return function loadSingleflight() {
if (inFlight) return inFlight

const derivation = Promise.resolve().then(loadState)
const settled = derivation.finally(() => {
if (inFlight === settled) inFlight = null
})
inFlight = settled
return settled
}
}

export function createProductionLifecycleHandler({ loadState, serializeState }) {
if (typeof loadState !== 'function' || typeof serializeState !== 'function') {
throw new TypeError('loadState and serializeState must be functions')
}

return async function handler(request, response) {
response.setHeader('Cache-Control', 'private, no-store, max-age=0')
response.setHeader('Netlify-CDN-Cache-Control', 'no-store')

if (request.method !== 'GET') {
response.setHeader('Allow', 'GET')
return response.status(405).json({ error: 'Method not allowed' })
}

let state = null
try {
state = await loadState()
} catch (_error) {
state = null
}
return response.status(200).json(serializeState(state))
}
}
20 changes: 20 additions & 0 deletions lib/publicJsonArtifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@ export const MAX_JSON_SEARCH_RESULTS = 100
* artifact requires an intentional registry update in the same review.
*/
export const PUBLIC_JSON_ARTIFACTS = Object.freeze({
'/production-lifecycle.json': Object.freeze({
source: '/production-lifecycle.json',
fileName: 'production-lifecycle.json',
format: 'json',
title: 'OpenAdapt Production admission projection',
description:
'The read-time Production projection for all seven product targets, bound to exact canonical source commits and hashes.',
// The projection changes when the admission ledger changes. Its own
// source inventory binds every canonical input by SHA-256.
sha256: null,
}),
'/schemas/production-lifecycle-public.schema.json': Object.freeze({
source: '/schemas/production-lifecycle-public.schema.json',
fileName: 'production-lifecycle-public.schema.json',
format: 'json',
title: 'Production lifecycle projection schema',
description:
'The exact JSON Schema for the public Production admission projection.',
sha256: 'ac0ee5ca860aa264debe8f3abdbb3c67b214c9fdfae9a55bd21557ac9a0ab64f',
}),
'/status.json': Object.freeze({
source: '/status.json',
fileName: 'status.json',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"scripts": {
"dev": "next dev",
"check:lifecycle": "python3 -S scripts/render_production_lifecycle.py --check",
"check:versions": "node scripts/check_published_version_claims.mjs",
"fetch:paper": "node scripts/fetch-paper-pdf.js",
"verify:presentations": "node scripts/verify-reference-presentations.mjs",
Expand Down
18 changes: 18 additions & 0 deletions pages/api/production-lifecycle-state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import projection from '../../public/production-lifecycle.json'
import {
createProductionLifecycleHandler,
createServerFetch,
createSingleflightLoader,
} from '../../lib/productionLifecycleEndpoint'

const lifecycle = require('../../public/production-lifecycle.js')

const serverFetch = createServerFetch()
const loadProductionLifecycleState = createSingleflightLoader(() =>
lifecycle.loadCanonical(serverFetch, projection, Date.now())
)

export default createProductionLifecycleHandler({
loadState: loadProductionLifecycleState,
serializeState: lifecycle.serializeState,
})
Loading
Loading