Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@devrunbook/application",
"version": "0.1.0",
"private": true,
"type": "module",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"lint": "eslint src --max-warnings=0",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@devrunbook/composer": "workspace:*",
"@devrunbook/domain": "workspace:*",
"@devrunbook/repository-intel": "workspace:*"
},
"devDependencies": {
"@types/node": "24.13.3",
"typescript": "5.9.3",
"vitest": "4.1.10",
"yaml": "2.9.0"
}
}
@@ -0,0 +1,192 @@
import type { GeneratedRun, WorkspaceAuthorizationLookup } from '..'
import { describe, expect, it, vi } from 'vitest'
import type {
GeneratedArtifactMetadata,
GeneratedArtifactMetadataStore,
ImmutableArtifactStorage,
} from './generated-artifact'
import { exportGeneratedRunArtifact } from './export-generated-run-artifact'
const userId = '00000000-0000-4000-8000-000000000001'
const workspaceId = '00000000-0000-4000-8000-000000000002'
const runId = '00000000-0000-4000-8000-000000000003'
const run: GeneratedRun = {
id: runId,
workspaceId,
generatedBy: userId,
sourceDraftId: null,
playbookVersionId: '00000000-0000-4000-8000-000000000004',
snapshots: {
playbook: { slug: 'root-cause-bugfix' },
repositoryProfile: null,
normalizedInput: {},
policy: {},
provenance: [],
},
lint: { exportReadiness: 'ready', findings: [] },
renderedPrompt: '# Mission\n\nRepair it.\n',
renderDigest: 'a'.repeat(64),
idempotencyKey: 'run-generation',
generatedAt: '2026-07-27T12:00:00.000Z',
}
class MemoryMetadata implements GeneratedArtifactMetadataStore {
artifact: GeneratedArtifactMetadata | null = null
async createIdempotently(candidate: GeneratedArtifactMetadata) {
if (!this.artifact) {
this.artifact = candidate
return { artifact: candidate, created: true }
}
return { artifact: this.artifact, created: false }
}
async findByIdInWorkspace(id: string, requestedWorkspaceId: string) {
return this.artifact?.id === id &&
this.artifact.workspaceId === requestedWorkspaceId
? this.artifact
: null
}
async listByRunInWorkspace(
requestedRunId: string,
requestedWorkspaceId: string,
) {
return this.artifact?.runId === requestedRunId &&
this.artifact.workspaceId === requestedWorkspaceId
? [this.artifact]
: []
}
}
class MemoryStorage implements ImmutableArtifactStorage {
readonly bytes = new Map<string, Uint8Array>()
async putImmutable(key: string, content: Uint8Array) {
const created = !this.bytes.has(key)
this.bytes.set(key, new Uint8Array(content))
return { created }
}
async read(key: string) {
const content = this.bytes.get(key)
if (!content) throw new Error('missing')
return new Uint8Array(content)
}
}
function authorization(
role: 'viewer' | 'editor',
): WorkspaceAuthorizationLookup {
return {
findWorkspaceAuthorization: vi.fn(async () => ({
userId,
workspaceId,
instanceRole: 'user' as const,
workspaceRole: role,
userStatus: 'active' as const,
})),
}
}
function dependencies(role: 'viewer' | 'editor' = 'editor') {
return {
authorization: authorization(role),
metadata: new MemoryMetadata(),
storage: new MemoryStorage(),
runs: { findByIdForWorkspace: vi.fn(async () => run) },
now: () => new Date('2026-07-28T12:00:00.000Z'),
maxArtifactBytes: 1_000_000,
retentionDays: 90,
markdownRenderer: {
render: vi.fn(async (historicalRun: GeneratedRun) => ({
content: new TextEncoder().encode(
`<!-- task -->\n\n${historicalRun.renderedPrompt}`,
),
filename: 'DevRunbook-root-cause-bugfix-TASK.md',
mediaType: 'text/markdown; charset=utf-8',
})),
},
}
}
describe('authoritative generated-run artifact export', () => {
it.each([
['prompt_text', 'text/plain; charset=utf-8', '.txt'],
['markdown', 'text/markdown; charset=utf-8', '.md'],
] as const)(
'renders %s directly from immutable prompt bytes',
async (type, mediaType, extension) => {
const deps = dependencies()
const first = await exportGeneratedRunArtifact(deps, {
actor: { userId },
workspaceId,
runId,
artifactType: type,
idempotencyKey: `export-${type}`,
})
const second = await exportGeneratedRunArtifact(deps, {
actor: { userId },
workspaceId,
runId,
artifactType: type,
idempotencyKey: `export-${type}`,
})
expect(first.created).toBe(true)
expect(second.created).toBe(false)
expect(first.artifact.id).toBe(second.artifact.id)
expect(first.artifact.mediaType).toBe(mediaType)
expect(first.artifact.filename).toMatch(new RegExp(`\\${extension}$`))
expect(first.artifact.expiresAt).toBe('2026-10-26T12:00:00.000Z')
const expectedContent =
type === 'markdown'
? `<!-- task -->\n\n${run.renderedPrompt}`
: run.renderedPrompt
expect(
new TextDecoder().decode(
await deps.storage.read(first.artifact.storageKey),
),
).toBe(expectedContent)
},
)
it('denies viewer creation before loading or rendering the run', async () => {
const deps = dependencies('viewer')
await expect(
exportGeneratedRunArtifact(deps, {
actor: { userId },
workspaceId,
runId,
artifactType: 'markdown',
idempotencyKey: 'viewer-export',
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
expect(deps.runs.findByIdForWorkspace).not.toHaveBeenCalled()
})
it('exposes an explicit run-pack degraded state and enforces size bounds', async () => {
const unavailable = dependencies()
await expect(
exportGeneratedRunArtifact(unavailable, {
actor: { userId },
workspaceId,
runId,
artifactType: 'run_pack_zip',
idempotencyKey: 'zip-export',
}),
).rejects.toMatchObject({ code: 'generated_artifact_renderer_unavailable' })
const bounded = { ...dependencies(), maxArtifactBytes: 2 }
await expect(
exportGeneratedRunArtifact(bounded, {
actor: { userId },
workspaceId,
runId,
artifactType: 'markdown',
idempotencyKey: 'bounded-export',
}),
).rejects.toMatchObject({ code: 'generated_artifact_too_large' })
})
})
@@ -0,0 +1,237 @@
import { createHash } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
} from '../auth/workspace-authorization'
import type { GeneratedRun } from '../generated-runs/create-generated-run'
import type { GeneratedRunReader } from '../generated-runs/get-generated-run'
import {
createGeneratedArtifact,
type GeneratedArtifactDependencies,
type StoreGeneratedArtifactResult,
} from './generated-artifact'
export const synchronousRunArtifactTypes = [
'prompt_text',
'markdown',
'run_pack_zip',
'agents_suggestion',
] as const
export type SynchronousRunArtifactType =
(typeof synchronousRunArtifactTypes)[number]
export interface RenderedRunArtifact {
readonly content: Uint8Array
readonly filename: string
readonly mediaType: string
}
export interface RunArtifactRenderer {
render(run: GeneratedRun): Promise<RenderedRunArtifact>
}
export interface ExportGeneratedRunArtifactDependencies extends GeneratedArtifactDependencies {
readonly runs: GeneratedRunReader
readonly maxArtifactBytes: number
readonly retentionDays: number
readonly markdownRenderer: RunArtifactRenderer
/** Injected by the Run Pack core; absence is an explicit degraded state. */
readonly runPackRenderer?: RunArtifactRenderer
readonly agentsSuggestionRenderer?: RunArtifactRenderer
}
export interface ExportGeneratedRunArtifactRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly runId: string
readonly artifactType: SynchronousRunArtifactType
readonly idempotencyKey: string
}
const uuidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
function deterministicArtifactId(
workspaceId: string,
runId: string,
artifactType: SynchronousRunArtifactType,
idempotencyKey: string,
): string {
const bytes = createHash('sha256')
.update('devrunbook:artifact-idempotency:v1\0', 'utf8')
.update(workspaceId, 'utf8')
.update('\0', 'utf8')
.update(runId, 'utf8')
.update('\0', 'utf8')
.update(artifactType, 'utf8')
.update('\0', 'utf8')
.update(idempotencyKey, 'utf8')
.digest()
.subarray(0, 16)
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x50
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80
const hex = bytes.toString('hex')
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
}
function safeSlug(run: GeneratedRun): string {
const raw = run.snapshots.playbook.slug
const normalized =
typeof raw === 'string'
? raw
.normalize('NFKD')
.toLowerCase()
.replace(/[^a-z0-9]+/gu, '-')
.replace(/^-+|-+$/gu, '')
.slice(0, 100)
: ''
return normalized || `generated-task-${run.id.slice(0, 8)}`
}
function textArtifact(
run: GeneratedRun,
markdown: boolean,
): RenderedRunArtifact {
const extension = markdown ? 'md' : 'txt'
return {
content: new TextEncoder().encode(run.renderedPrompt),
filename: `${safeSlug(run)}-${run.id.slice(0, 8)}.${extension}`,
mediaType: markdown
? 'text/markdown; charset=utf-8'
: 'text/plain; charset=utf-8',
}
}
async function renderArtifact(
dependencies: ExportGeneratedRunArtifactDependencies,
run: GeneratedRun,
artifactType: SynchronousRunArtifactType,
): Promise<RenderedRunArtifact> {
if (artifactType === 'prompt_text') return textArtifact(run, false)
if (artifactType === 'markdown') {
return dependencies.markdownRenderer.render(run)
}
const renderer =
artifactType === 'run_pack_zip'
? dependencies.runPackRenderer
: dependencies.agentsSuggestionRenderer
if (!renderer) {
throw new DomainError(
'generated_artifact_renderer_unavailable',
'Requested artifact generation is temporarily unavailable',
)
}
return renderer.render(run)
}
function expiresAt(now: Date, retentionDays: number): Date {
return new Date(now.getTime() + retentionDays * 86_400_000)
}
export async function exportGeneratedRunArtifact(
dependencies: ExportGeneratedRunArtifactDependencies,
request: ExportGeneratedRunArtifactRequest,
): Promise<StoreGeneratedArtifactResult> {
if (!uuidPattern.test(request.runId)) {
throw new DomainError(
'generated_artifact_run_not_found',
'Generated run was not found',
)
}
if (
request.idempotencyKey.length === 0 ||
request.idempotencyKey.length > 255 ||
request.idempotencyKey.trim() !== request.idempotencyKey ||
/[\0\r\n]/u.test(request.idempotencyKey)
) {
throw new DomainError(
'generated_artifact_idempotency_key_invalid',
'Idempotency key must contain 1 to 255 safe characters',
)
}
if (!synchronousRunArtifactTypes.includes(request.artifactType)) {
throw new DomainError(
'generated_artifact_type_invalid',
'Generated artifact type is not supported by this endpoint',
)
}
if (
!Number.isSafeInteger(dependencies.maxArtifactBytes) ||
dependencies.maxArtifactBytes < 1 ||
!Number.isSafeInteger(dependencies.retentionDays) ||
dependencies.retentionDays < 1
) {
throw new DomainError(
'generated_artifact_configuration_invalid',
'Generated artifact limits are invalid',
)
}
await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'write',
})
const run = await dependencies.runs.findByIdForWorkspace(
request.workspaceId,
request.runId,
)
if (!run) {
throw new DomainError(
'generated_artifact_run_not_found',
'Generated run was not found',
)
}
const rendered = await renderArtifact(dependencies, run, request.artifactType)
const expectedMediaType: Record<SynchronousRunArtifactType, string> = {
prompt_text: 'text/plain; charset=utf-8',
markdown: 'text/markdown; charset=utf-8',
run_pack_zip: 'application/zip',
agents_suggestion: 'text/markdown; charset=utf-8',
}
const expectedSuffix: Record<SynchronousRunArtifactType, string> = {
prompt_text: '.txt',
markdown: '.md',
run_pack_zip: '.zip',
agents_suggestion: '.suggested',
}
if (
rendered.mediaType !== expectedMediaType[request.artifactType] ||
!rendered.filename
.toLowerCase()
.endsWith(expectedSuffix[request.artifactType])
) {
throw new DomainError(
'generated_artifact_renderer_invalid',
'Artifact renderer returned unsafe metadata',
)
}
if (rendered.content.byteLength > dependencies.maxArtifactBytes) {
throw new DomainError(
'generated_artifact_too_large',
'Generated artifact exceeds the configured size limit',
)
}
return createGeneratedArtifact(dependencies, {
actor: request.actor,
workspaceId: request.workspaceId,
artifactId: deterministicArtifactId(
request.workspaceId,
request.runId,
request.artifactType,
request.idempotencyKey,
),
runId: request.runId,
artifactType: request.artifactType,
filename: rendered.filename,
mediaType: rendered.mediaType,
content: rendered.content,
expiresAt: expiresAt(dependencies.now(), dependencies.retentionDays),
})
}
@@ -0,0 +1,173 @@
import { describe, expect, it } from 'vitest'
import type {
GeneratedArtifactMetadata,
GeneratedArtifactMetadataStore,
ImmutableArtifactStorage,
StoreGeneratedArtifactResult,
WorkspaceAuthorizationRecord,
} from '..'
import {
createGeneratedArtifact,
downloadGeneratedArtifact,
} from './generated-artifact'
const workspaceId = '00000000-0000-4000-8000-000000000101'
const userId = '00000000-0000-4000-8000-000000000102'
const runId = '00000000-0000-4000-8000-000000000103'
const artifactId = '00000000-0000-4000-8000-000000000104'
class MemoryMetadata implements GeneratedArtifactMetadataStore {
artifact: GeneratedArtifactMetadata | null = null
async createIdempotently(
artifact: GeneratedArtifactMetadata,
): Promise<StoreGeneratedArtifactResult> {
if (!this.artifact) {
this.artifact = artifact
return { artifact, created: true }
}
return { artifact: this.artifact, created: false }
}
async findByIdInWorkspace(id: string, workspace: string) {
return this.artifact?.id === id && this.artifact.workspaceId === workspace
? this.artifact
: null
}
async listByRunInWorkspace(runId: string, workspace: string) {
return this.artifact?.runId === runId &&
this.artifact.workspaceId === workspace
? [this.artifact]
: []
}
}
class MemoryStorage implements ImmutableArtifactStorage {
readonly bytes = new Map<string, Uint8Array>()
async putImmutable(key: string, content: Uint8Array) {
const created = !this.bytes.has(key)
if (created) this.bytes.set(key, new Uint8Array(content))
return { created }
}
async read(key: string) {
const content = this.bytes.get(key)
if (!content) throw new Error('missing')
return new Uint8Array(content)
}
}
function authorization(role: 'viewer' | 'editor' | 'owner') {
return {
async findWorkspaceAuthorization(): Promise<WorkspaceAuthorizationRecord> {
return {
userId,
workspaceId,
instanceRole: 'user',
workspaceRole: role,
userStatus: 'active',
}
},
}
}
function request(content = new TextEncoder().encode('# Run\n')) {
return {
actor: { userId },
workspaceId,
artifactId,
runId,
artifactType: 'markdown' as const,
filename: 'run.md',
mediaType: 'text/markdown; charset=utf-8',
content,
}
}
describe('generated artifact use cases', () => {
it('creates immutable bytes and metadata idempotently', async () => {
const metadata = new MemoryMetadata()
const storage = new MemoryStorage()
const dependencies = {
authorization: authorization('editor'),
metadata,
storage,
now: () => new Date('2026-07-27T12:00:00.000Z'),
}
await expect(
createGeneratedArtifact(dependencies, request()),
).resolves.toMatchObject({ created: true })
await expect(
createGeneratedArtifact(dependencies, request()),
).resolves.toMatchObject({ created: false })
expect(metadata.artifact?.sha256).toHaveLength(64)
expect(metadata.artifact?.sizeBytes).toBe(6n)
expect(metadata.artifact?.storageKey).toMatch(/^[0-9a-f]{64}$/)
expect(storage.bytes).toHaveLength(1)
})
it('allows a viewer to download only from the authorized workspace', async () => {
const metadata = new MemoryMetadata()
const storage = new MemoryStorage()
await createGeneratedArtifact(
{
authorization: authorization('editor'),
metadata,
storage,
now: () => new Date('2026-07-27T12:00:00.000Z'),
},
request(),
)
const download = await downloadGeneratedArtifact(
{ authorization: authorization('viewer'), metadata, storage },
{ actor: { userId }, workspaceId, artifactId },
)
expect(new TextDecoder().decode(download.content)).toBe('# Run\n')
await expect(
downloadGeneratedArtifact(
{ authorization: authorization('viewer'), metadata, storage },
{
actor: { userId },
workspaceId: '00000000-0000-4000-8000-000000000999',
artifactId,
},
),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
})
it('rejects unsafe filenames, viewer creation, and corrupted bytes', async () => {
const metadata = new MemoryMetadata()
const storage = new MemoryStorage()
const base = {
authorization: authorization('editor'),
metadata,
storage,
now: () => new Date('2026-07-27T12:00:00.000Z'),
}
await expect(
createGeneratedArtifact(base, { ...request(), filename: '../run.md' }),
).rejects.toMatchObject({ code: 'generated_artifact_filename_invalid' })
await expect(
createGeneratedArtifact(
{ ...base, authorization: authorization('viewer') },
request(),
),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await createGeneratedArtifact(base, request())
const key = metadata.artifact?.storageKey
if (!key) throw new Error('expected storage key')
storage.bytes.set(key, new TextEncoder().encode('tampered'))
await expect(
downloadGeneratedArtifact(
{ authorization: authorization('viewer'), metadata, storage },
{ actor: { userId }, workspaceId, artifactId },
),
).rejects.toMatchObject({ code: 'generated_artifact_integrity_failed' })
})
})
@@ -0,0 +1,263 @@
import { createHash } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
export const generatedArtifactTypes = [
'prompt_text',
'markdown',
'run_pack_zip',
'agents_suggestion',
'support_bundle',
] as const
export type GeneratedArtifactType = (typeof generatedArtifactTypes)[number]
export interface GeneratedArtifactMetadata {
readonly id: string
readonly workspaceId: string
readonly runId: string
readonly artifactType: GeneratedArtifactType
readonly storageKey: string
readonly filename: string
readonly mediaType: string
readonly sizeBytes: bigint
readonly sha256: string
readonly expiresAt: string | null
readonly createdAt: string
}
export interface StoreGeneratedArtifactResult {
readonly artifact: GeneratedArtifactMetadata
readonly created: boolean
}
export interface GeneratedArtifactMetadataStore {
createIdempotently(
artifact: GeneratedArtifactMetadata,
): Promise<StoreGeneratedArtifactResult>
findByIdInWorkspace(
id: string,
workspaceId: string,
): Promise<GeneratedArtifactMetadata | null>
listByRunInWorkspace(
runId: string,
workspaceId: string,
): Promise<readonly GeneratedArtifactMetadata[]>
}
export interface ImmutableArtifactStorage {
putImmutable(
storageKey: string,
content: Uint8Array,
sha256: string,
): Promise<{ readonly created: boolean }>
read(storageKey: string): Promise<Uint8Array>
}
export interface CreateGeneratedArtifactRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly artifactId: string
readonly runId: string
readonly artifactType: GeneratedArtifactType
readonly filename: string
readonly mediaType: string
readonly content: Uint8Array
readonly expiresAt?: Date | null
}
export interface GeneratedArtifactDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly metadata: GeneratedArtifactMetadataStore
readonly storage: ImmutableArtifactStorage
readonly now: () => Date
}
export interface DownloadGeneratedArtifactRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly artifactId: string
}
export interface GeneratedArtifactDownload {
readonly artifact: GeneratedArtifactMetadata
readonly content: Uint8Array
}
const uuidPattern =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const digestPattern = /^[0-9a-f]{64}$/
function digest(content: Uint8Array): string {
return createHash('sha256').update(content).digest('hex')
}
function storageKeyForArtifact(id: string): string {
return createHash('sha256')
.update(`devrunbook:generated-artifact:${id}`, 'utf8')
.digest('hex')
}
function assertCreateRequest(request: CreateGeneratedArtifactRequest): void {
if (
!uuidPattern.test(request.artifactId) ||
!uuidPattern.test(request.runId)
) {
throw new DomainError(
'generated_artifact_identifier_invalid',
'Artifact and run identifiers must be UUIDs',
)
}
if (!generatedArtifactTypes.includes(request.artifactType)) {
throw new DomainError(
'generated_artifact_type_invalid',
'Generated artifact type is not supported',
)
}
if (
request.filename.length === 0 ||
request.filename.length > 255 ||
request.filename === '.' ||
request.filename === '..' ||
/[\\/\0\r\n]/u.test(request.filename)
) {
throw new DomainError(
'generated_artifact_filename_invalid',
'Artifact filename must be a safe basename',
)
}
if (
request.mediaType.length === 0 ||
request.mediaType.length > 255 ||
/[\0\r\n]/u.test(request.mediaType)
) {
throw new DomainError(
'generated_artifact_media_type_invalid',
'Artifact media type is invalid',
)
}
}
function assertStoredArtifactMatches(
candidate: GeneratedArtifactMetadata,
stored: GeneratedArtifactMetadata,
): void {
if (
stored.id !== candidate.id ||
stored.workspaceId !== candidate.workspaceId ||
stored.runId !== candidate.runId ||
stored.artifactType !== candidate.artifactType ||
stored.storageKey !== candidate.storageKey ||
stored.filename !== candidate.filename ||
stored.mediaType !== candidate.mediaType ||
stored.sizeBytes !== candidate.sizeBytes ||
stored.sha256 !== candidate.sha256
) {
throw new DomainError(
'generated_artifact_store_invariant_failed',
'Artifact store returned metadata that does not match the immutable request',
)
}
}
export async function createGeneratedArtifact(
dependencies: GeneratedArtifactDependencies,
request: CreateGeneratedArtifactRequest,
): Promise<StoreGeneratedArtifactResult> {
assertCreateRequest(request)
await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'write',
})
const content = new Uint8Array(request.content)
const sha256 = digest(content)
const candidate: GeneratedArtifactMetadata = Object.freeze({
id: request.artifactId,
workspaceId: request.workspaceId,
runId: request.runId,
artifactType: request.artifactType,
storageKey: storageKeyForArtifact(request.artifactId),
filename: request.filename,
mediaType: request.mediaType,
sizeBytes: BigInt(content.byteLength),
sha256,
expiresAt: request.expiresAt?.toISOString() ?? null,
createdAt: dependencies.now().toISOString(),
})
await dependencies.storage.putImmutable(candidate.storageKey, content, sha256)
const result = await dependencies.metadata.createIdempotently(candidate)
assertStoredArtifactMatches(candidate, result.artifact)
return Object.freeze({
artifact: Object.freeze({ ...result.artifact }),
created: result.created,
})
}
export async function downloadGeneratedArtifact(
dependencies: Pick<
GeneratedArtifactDependencies,
'authorization' | 'metadata' | 'storage'
> & { readonly now?: () => Date },
request: DownloadGeneratedArtifactRequest,
): Promise<GeneratedArtifactDownload> {
await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'read',
})
const artifact = await dependencies.metadata.findByIdInWorkspace(
request.artifactId,
request.workspaceId,
)
if (!artifact) {
throw new DomainError(
'generated_artifact_not_found',
'Generated artifact was not found',
)
}
if (artifact.expiresAt !== null) {
const expiry = new Date(artifact.expiresAt).getTime()
if (!Number.isFinite(expiry)) {
throw new DomainError(
'generated_artifact_integrity_failed',
'Generated artifact expiry metadata is invalid',
)
}
if (expiry <= (dependencies.now?.() ?? new Date()).getTime()) {
throw new DomainError(
'generated_artifact_expired',
'Generated artifact has expired',
)
}
}
if (!digestPattern.test(artifact.sha256)) {
throw new DomainError(
'generated_artifact_integrity_failed',
'Generated artifact metadata has an invalid digest',
)
}
const content = await dependencies.storage.read(artifact.storageKey)
const computed = digest(content)
if (
computed !== artifact.sha256 ||
BigInt(content.byteLength) !== artifact.sizeBytes
) {
throw new DomainError(
'generated_artifact_integrity_failed',
'Generated artifact bytes do not match immutable metadata',
)
}
return Object.freeze({
artifact: Object.freeze({ ...artifact }),
content: new Uint8Array(content),
})
}
@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest'
import {
AuthService,
type AuthPersistence,
type AuthSessionRecord,
type AuthUserRecord,
type CreateAuthSessionRecord,
} from './auth-service'
import { TokenDigester } from './token-digest'
class MemoryAuthPersistence implements AuthPersistence {
readonly user: AuthUserRecord = {
id: 'user-1',
email: 'owner@example.test',
displayName: 'Owner',
passwordHash: 'hash',
emailVerified: true,
image: null,
status: 'active',
createdAt: new Date('2026-01-01T00:00:00Z'),
updatedAt: new Date('2026-01-01T00:00:00Z'),
}
session: AuthSessionRecord | null = null
async findUserById(id: string) {
return id === this.user.id ? this.user : null
}
async findUserByEmail(email: string) {
return email === this.user.email ? this.user : null
}
async updateUser() {
return this.user
}
async createSession(input: CreateAuthSessionRecord) {
this.session = { id: 'session-1', revokedAt: null, ...input }
return this.session
}
async findSessionByTokenHash(tokenHash: string) {
return this.session?.tokenHash === tokenHash ? this.session : null
}
async touchSession(
id: string,
input: { lastSeenAt: Date; idleExpiresAt: Date },
) {
if (!this.session || this.session.id !== id) return null
this.session = { ...this.session, ...input }
return this.session
}
async revokeSessionByTokenHash(tokenHash: string, revokedAt: Date) {
if (!this.session || this.session.tokenHash !== tokenHash) return false
this.session = { ...this.session, revokedAt }
return true
}
async revokeSessionsForUser(userId: string, revokedAt: Date) {
if (!this.session || this.session.userId !== userId) return 0
this.session = { ...this.session, revokedAt }
return 1
}
}
describe('AuthService', () => {
it('stores only an HMAC digest and enforces sliding idle plus absolute expiry', async () => {
const persistence = new MemoryAuthPersistence()
let now = new Date('2026-01-01T00:00:00Z')
const service = new AuthService(
persistence,
new TokenDigester(Buffer.alloc(32, 4)),
() => now,
)
const rawToken = 'raw-session-token-with-enough-entropy'
const created = await service.createSession({ userId: 'user-1', rawToken })
expect(created?.rawToken).toBe(rawToken)
expect(persistence.session?.tokenHash).not.toContain(rawToken)
expect(persistence.session?.idleExpiresAt.toISOString()).toBe(
'2026-01-01T12:00:00.000Z',
)
expect(persistence.session?.absoluteExpiresAt.toISOString()).toBe(
'2026-01-31T00:00:00.000Z',
)
now = new Date('2026-01-01T11:00:00Z')
await expect(service.findActiveSession(rawToken)).resolves.not.toBeNull()
expect(persistence.session?.idleExpiresAt.toISOString()).toBe(
'2026-01-01T23:00:00.000Z',
)
})
it('revokes logout immediately and preserves the revocation timestamp', async () => {
const persistence = new MemoryAuthPersistence()
const now = new Date('2026-01-01T00:00:00Z')
const service = new AuthService(
persistence,
new TokenDigester(Buffer.alloc(32, 5)),
() => now,
)
const token = 'another-high-entropy-session-token'
await service.createSession({ userId: 'user-1', rawToken: token })
await expect(service.revokeSession(token)).resolves.toBe(true)
expect(persistence.session?.revokedAt).toEqual(now)
await expect(service.findActiveSession(token)).resolves.toBeNull()
})
})
@@ -0,0 +1,148 @@
import { isSessionActive, resolveSessionDeadlines } from './session-policy'
import type { TokenDigester } from './token-digest'
export interface AuthUserRecord {
id: string
email: string
displayName: string
passwordHash: string
emailVerified: boolean
image: string | null
status: 'active' | 'disabled' | 'pending_deletion'
createdAt: Date
updatedAt: Date
}
export interface AuthSessionRecord {
id: string
userId: string
tokenHash: string
createdAt: Date
lastSeenAt: Date
idleExpiresAt: Date
absoluteExpiresAt: Date
revokedAt: Date | null
sourceIpHash: string | null
userAgentSummary: string | null
}
export interface CreateAuthSessionRecord {
userId: string
tokenHash: string
createdAt: Date
lastSeenAt: Date
idleExpiresAt: Date
absoluteExpiresAt: Date
sourceIpHash: string | null
userAgentSummary: string | null
}
export interface AuthPersistence {
findUserById(id: string): Promise<AuthUserRecord | null>
findUserByEmail(email: string): Promise<AuthUserRecord | null>
updateUser(
id: string,
update: Partial<
Pick<
AuthUserRecord,
'displayName' | 'email' | 'emailVerified' | 'image' | 'passwordHash'
>
>,
): Promise<AuthUserRecord | null>
createSession(input: CreateAuthSessionRecord): Promise<AuthSessionRecord>
findSessionByTokenHash(tokenHash: string): Promise<AuthSessionRecord | null>
touchSession(
id: string,
input: { lastSeenAt: Date; idleExpiresAt: Date },
): Promise<AuthSessionRecord | null>
revokeSessionByTokenHash(tokenHash: string, revokedAt: Date): Promise<boolean>
revokeSessionsForUser(userId: string, revokedAt: Date): Promise<number>
}
export interface ActiveAuthSession {
session: AuthSessionRecord
user: AuthUserRecord
rawToken: string
}
export class AuthService {
constructor(
private readonly persistence: AuthPersistence,
private readonly digester: TokenDigester,
private readonly now: () => Date = () => new Date(),
) {}
findUserById(id: string) {
return this.persistence.findUserById(id)
}
findUserByEmail(email: string) {
return this.persistence.findUserByEmail(email.trim().toLowerCase())
}
updateUser(
id: string,
update: Partial<
Pick<
AuthUserRecord,
'displayName' | 'email' | 'emailVerified' | 'image' | 'passwordHash'
>
>,
) {
return this.persistence.updateUser(id, update)
}
async createSession(input: {
userId: string
rawToken: string
sourceIpHash?: string | null
userAgentSummary?: string | null
}): Promise<ActiveAuthSession | null> {
const user = await this.persistence.findUserById(input.userId)
if (!user || user.status !== 'active') return null
const now = this.now()
const deadlines = resolveSessionDeadlines({ createdAt: now, now })
const session = await this.persistence.createSession({
userId: user.id,
tokenHash: this.digester.digest(input.rawToken),
createdAt: now,
lastSeenAt: now,
...deadlines,
sourceIpHash: input.sourceIpHash ?? null,
userAgentSummary: input.userAgentSummary ?? null,
})
return { session, user, rawToken: input.rawToken }
}
async findActiveSession(rawToken: string): Promise<ActiveAuthSession | null> {
const now = this.now()
const session = await this.persistence.findSessionByTokenHash(
this.digester.digest(rawToken),
)
if (!session || !isSessionActive(now, session)) return null
const user = await this.persistence.findUserById(session.userId)
if (!user || user.status !== 'active') return null
const deadlines = resolveSessionDeadlines({
createdAt: session.createdAt,
now,
absoluteExpiresAt: session.absoluteExpiresAt,
})
const touched = await this.persistence.touchSession(session.id, {
lastSeenAt: now,
idleExpiresAt: deadlines.idleExpiresAt,
})
if (!touched) return null
return { session: touched, user, rawToken }
}
revokeSession(rawToken: string): Promise<boolean> {
return this.persistence.revokeSessionByTokenHash(
this.digester.digest(rawToken),
this.now(),
)
}
revokeSessionsForUser(userId: string): Promise<number> {
return this.persistence.revokeSessionsForUser(userId, this.now())
}
}
@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest'
import { TokenDigester } from './token-digest'
import {
acceptInvitation,
issueInvitation,
type InvitationStore,
type InvitationTransaction,
} from './invitations'
const token = 'invitation-token-with-sufficient-entropy'
const digester = new TokenDigester(Buffer.alloc(32, 7))
class FakeStore implements InvitationStore, InvitationTransaction {
created: Parameters<InvitationTransaction['create']>[0] | undefined
consumed: Parameters<InvitationTransaction['consume']>[0] | undefined
isConsumable(tokenHash: string) {
return Promise.resolve(tokenHash === digester.digest(token))
}
transaction<T>(work: (transaction: InvitationTransaction) => Promise<T>) {
return work(this)
}
create(input: Parameters<InvitationTransaction['create']>[0]) {
this.created = input
return Promise.resolve({
id: 'invite-1',
email: input.email,
expiresAt: input.expiresAt,
})
}
consume(input: Parameters<InvitationTransaction['consume']>[0]) {
this.consumed = input
return Promise.resolve({ userId: 'user-1' })
}
}
describe('invitations', () => {
it('stores only a digest and returns the raw token in a URL fragment', async () => {
const store = new FakeStore()
const result = await issueInvitation(
{
store,
digester,
publicBaseUrl: 'https://runbook.example.test',
now: () => new Date('2026-07-27T12:00:00.000Z'),
generateToken: () => token,
},
{
actorUserId: 'owner-1',
email: 'USER@Example.test',
instanceRole: 'user',
},
)
expect(result.inviteUrl).toBe(
`https://runbook.example.test/accept-invitation#token=${token}`,
)
expect(store.created?.tokenHash).toBe(digester.digest(token))
expect(JSON.stringify(store.created)).not.toContain(token)
expect(store.created?.email).toBe('user@example.test')
})
it('consumes the token digest with the supplied password hash', async () => {
const store = new FakeStore()
await expect(
acceptInvitation(
{ store, digester, now: () => new Date('2026-07-27T12:00:00.000Z') },
{
rawToken: token,
displayName: ' New User ',
passwordHash: 'safe-hash',
},
),
).resolves.toEqual({ userId: 'user-1' })
expect(store.consumed).toMatchObject({
tokenHash: digester.digest(token),
displayName: 'New User',
passwordHash: 'safe-hash',
})
})
})
@@ -0,0 +1,127 @@
import { randomBytes } from 'node:crypto'
import type { TokenDigester } from './token-digest'
export type InvitationInstanceRole = 'instance_admin' | 'user'
export type InvitationWorkspaceRole = 'owner' | 'editor' | 'viewer'
export interface InvitationRecord {
readonly id: string
readonly email: string
readonly expiresAt: Date
}
export interface InvitationTransaction {
create(input: {
readonly actorUserId: string
readonly email: string
readonly tokenHash: string
readonly instanceRole: InvitationInstanceRole
readonly workspaceId: string | null
readonly workspaceRole: InvitationWorkspaceRole | null
readonly expiresAt: Date
}): Promise<InvitationRecord>
consume(input: {
readonly tokenHash: string
readonly displayName: string
readonly passwordHash: string
readonly acceptedAt: Date
}): Promise<{ readonly userId: string } | null>
}
export interface InvitationStore {
isConsumable(tokenHash: string, now: Date): Promise<boolean>
transaction<T>(
work: (transaction: InvitationTransaction) => Promise<T>,
): Promise<T>
}
export class InvitationError extends Error {
constructor(readonly code: 'invalid_invitation' | 'invitation_conflict') {
super(
code === 'invalid_invitation'
? 'Invitation is invalid'
: 'Invitation conflicts with existing identity data',
)
this.name = 'InvitationError'
}
}
export async function issueInvitation(
dependencies: {
readonly store: InvitationStore
readonly digester: TokenDigester
readonly publicBaseUrl: string
readonly now?: () => Date
readonly generateToken?: () => string
},
input: {
readonly actorUserId: string
readonly email: string
readonly instanceRole: InvitationInstanceRole
readonly workspaceId?: string | null
readonly workspaceRole?: InvitationWorkspaceRole | null
},
): Promise<InvitationRecord & { readonly inviteUrl: string }> {
const now = (dependencies.now ?? (() => new Date()))()
const rawToken = (
dependencies.generateToken ?? (() => randomBytes(32).toString('base64url'))
)()
const workspaceId = input.workspaceId ?? null
const workspaceRole = input.workspaceRole ?? null
if ((workspaceId === null) !== (workspaceRole === null)) {
throw new InvitationError('invalid_invitation')
}
const invitation = await dependencies.store.transaction((transaction) =>
transaction.create({
actorUserId: input.actorUserId,
email: input.email.trim().toLowerCase(),
tokenHash: dependencies.digester.digest(rawToken),
instanceRole: input.instanceRole,
workspaceId,
workspaceRole,
expiresAt: new Date(now.getTime() + 24 * 60 * 60 * 1_000),
}),
)
const inviteUrl = new URL('/accept-invitation', dependencies.publicBaseUrl)
inviteUrl.hash = new URLSearchParams({ token: rawToken }).toString()
return Object.freeze({ ...invitation, inviteUrl: inviteUrl.toString() })
}
export async function acceptInvitation(
dependencies: {
readonly store: InvitationStore
readonly digester: TokenDigester
readonly now?: () => Date
},
input: {
readonly rawToken: string
readonly displayName: string
readonly passwordHash: string
},
) {
const result = await dependencies.store.transaction((transaction) =>
transaction.consume({
tokenHash: dependencies.digester.digest(input.rawToken),
displayName: input.displayName.trim(),
passwordHash: input.passwordHash,
acceptedAt: (dependencies.now ?? (() => new Date()))(),
}),
)
if (!result) throw new InvitationError('invalid_invitation')
return result
}
export function isInvitationConsumable(
dependencies: {
readonly store: InvitationStore
readonly digester: TokenDigester
readonly now?: () => Date
},
rawToken: string,
) {
return dependencies.store.isConsumable(
dependencies.digester.digest(rawToken),
(dependencies.now ?? (() => new Date()))(),
)
}
@@ -0,0 +1,182 @@
import { describe, expect, it } from 'vitest'
import { TokenDigester } from '../token-digest'
import {
consumePasswordResetToken,
isPasswordResetTokenConsumable,
issueOperatorPasswordResetToken,
type PasswordResetStore,
type PasswordResetTransaction,
} from './password-reset'
class MemoryTransaction implements PasswordResetTransaction {
readonly user = { id: 'user-1', email: 'owner@example.test' }
readonly tokens: Array<{
id: string
userId: string
tokenHash: string
expiresAt: Date
usedAt: Date | null
}> = []
readonly audit: Array<{
action: string
metadata: Readonly<Record<string, string | number>>
}> = []
passwordHash = 'old-hash'
revokedSessions = 0
async findActiveUserByEmail(email: string) {
return email === this.user.email ? this.user : null
}
async revokeUnusedTokens(userId: string, revokedAt: Date) {
let count = 0
for (const token of this.tokens) {
if (token.userId === userId && token.usedAt === null) {
token.usedAt = revokedAt
count++
}
}
return count
}
async createToken(input: {
userId: string
tokenHash: string
expiresAt: Date
}) {
const token = {
id: `token-${this.tokens.length + 1}`,
userId: input.userId,
tokenHash: input.tokenHash,
expiresAt: input.expiresAt,
usedAt: null,
}
this.tokens.push(token)
return { id: token.id }
}
async hasConsumableToken(input: { tokenHash: string; checkedAt: Date }) {
return this.tokens.some(
(candidate) =>
candidate.tokenHash === input.tokenHash &&
candidate.usedAt === null &&
candidate.expiresAt > input.checkedAt,
)
}
async consumeValidToken(input: { tokenHash: string; consumedAt: Date }) {
const token = this.tokens.find(
(candidate) =>
candidate.tokenHash === input.tokenHash &&
candidate.usedAt === null &&
candidate.expiresAt > input.consumedAt,
)
if (!token) return null
token.usedAt = input.consumedAt
return { id: token.id, userId: token.userId }
}
async updatePassword(input: { passwordHash: string }) {
this.passwordHash = input.passwordHash
return true
}
async revokeSessions() {
this.revokedSessions += 2
return 2
}
async appendAuditEvent(input: {
action: string
metadata: Readonly<Record<string, string | number>>
}) {
this.audit.push(input)
}
}
function fixture() {
const transaction = new MemoryTransaction()
const store: PasswordResetStore = {
transaction: (work) => work(transaction),
}
return {
transaction,
dependencies: {
store,
digester: new TokenDigester(Buffer.alloc(32, 7)),
now: () => new Date('2026-07-27T12:00:00.000Z'),
generateToken: () => 'raw-operator-reset-token-with-entropy',
},
}
}
describe('password reset boundary', () => {
it('returns one raw URL while storing only its digest and revoking prior tokens', async () => {
const { dependencies, transaction } = fixture()
transaction.tokens.push({
id: 'old-token',
userId: 'user-1',
tokenHash: 'old-digest',
expiresAt: new Date('2026-07-27T13:00:00.000Z'),
usedAt: null,
})
const issued = await issueOperatorPasswordResetToken(dependencies, {
email: ' Owner@Example.Test ',
publicBaseUrl: 'https://runbook.example.test',
})
expect(issued.resetUrl).toBe(
'https://runbook.example.test/reset-password#token=raw-operator-reset-token-with-entropy',
)
expect(transaction.tokens[0]?.usedAt).not.toBeNull()
expect(transaction.tokens[1]?.tokenHash).not.toContain(
'raw-operator-reset-token-with-entropy',
)
expect(transaction.audit[0]).toMatchObject({
action: 'user.password_reset.issued',
metadata: { channel: 'operator', revokedPriorTokens: 1 },
})
})
it('consumes once, stores the Better Auth hash, revokes sessions and audits', async () => {
const { dependencies, transaction } = fixture()
const issued = await issueOperatorPasswordResetToken(dependencies, {
email: 'owner@example.test',
publicBaseUrl: 'https://runbook.example.test',
})
const rawToken = new URLSearchParams(
new URL(issued.resetUrl).hash.slice(1),
).get('token')
expect(rawToken).toBeTruthy()
await expect(
isPasswordResetTokenConsumable(dependencies, rawToken ?? ''),
).resolves.toBe(true)
await consumePasswordResetToken(dependencies, {
rawToken: rawToken ?? '',
betterAuthPasswordHash: 'better-auth-produced-hash',
})
expect(transaction.passwordHash).toBe('better-auth-produced-hash')
expect(transaction.revokedSessions).toBe(2)
expect(transaction.audit[1]).toMatchObject({
action: 'user.password_reset.completed',
metadata: { revokedSessions: 2 },
})
await expect(
consumePasswordResetToken(dependencies, {
rawToken: rawToken ?? '',
betterAuthPasswordHash: 'another-better-auth-hash',
}),
).rejects.toMatchObject({ code: 'password_reset_token_invalid' })
await expect(
isPasswordResetTokenConsumable(dependencies, rawToken ?? ''),
).resolves.toBe(false)
})
it('rejects public URLs containing credentials before persistence', async () => {
const { dependencies, transaction } = fixture()
await expect(
issueOperatorPasswordResetToken(dependencies, {
email: 'owner@example.test',
publicBaseUrl: 'https://operator:secret@runbook.example.test',
}),
).rejects.toMatchObject({ code: 'password_reset_public_url_invalid' })
expect(transaction.tokens).toEqual([])
})
})
@@ -0,0 +1,227 @@
import { randomBytes } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import type { TokenDigester } from '../token-digest'
export interface PasswordResetUser {
readonly id: string
readonly email: string
}
export interface PasswordResetTransaction {
findActiveUserByEmail(email: string): Promise<PasswordResetUser | null>
revokeUnusedTokens(userId: string, revokedAt: Date): Promise<number>
createToken(input: {
userId: string
tokenHash: string
expiresAt: Date
createdBy: string | null
createdAt: Date
}): Promise<{ id: string }>
hasConsumableToken(input: {
tokenHash: string
checkedAt: Date
}): Promise<boolean>
consumeValidToken(input: {
tokenHash: string
consumedAt: Date
}): Promise<{ id: string; userId: string } | null>
updatePassword(input: {
userId: string
passwordHash: string
changedAt: Date
}): Promise<boolean>
revokeSessions(userId: string, revokedAt: Date): Promise<number>
appendAuditEvent(input: {
actorUserId: string | null
action: 'user.password_reset.issued' | 'user.password_reset.completed'
resourceId: string
metadata: Readonly<Record<string, string | number>>
}): Promise<void>
}
export interface PasswordResetStore {
transaction<T>(
work: (transaction: PasswordResetTransaction) => Promise<T>,
): Promise<T>
}
export interface PasswordResetDependencies {
readonly store: PasswordResetStore
readonly digester: TokenDigester
readonly now?: () => Date
readonly generateToken?: () => string
}
export interface IssueOperatorPasswordResetRequest {
readonly email: string
readonly publicBaseUrl: string
readonly createdBy?: string | null
readonly expiresInSeconds?: number
}
export interface IssuedPasswordReset {
/** Contains the secret once; never persist or log this URL. */
readonly resetUrl: string
readonly expiresAt: Date
}
export interface ConsumePasswordResetRequest {
readonly rawToken: string
/** Must be produced by the configured local-identity password hasher. */
readonly betterAuthPasswordHash: string
}
export async function isPasswordResetTokenConsumable(
dependencies: PasswordResetDependencies,
rawToken: string,
): Promise<boolean> {
let tokenHash: string
try {
tokenHash = dependencies.digester.digest(rawToken)
} catch {
return false
}
const checkedAt = dependencies.now?.() ?? new Date()
return dependencies.store.transaction((transaction) =>
transaction.hasConsumableToken({ tokenHash, checkedAt }),
)
}
const defaultExpirySeconds = 30 * 60
function normalizedEmail(email: string): string {
const normalized = email.trim().toLowerCase()
if (!normalized.includes('@') || normalized.length > 320) {
throw new DomainError(
'password_reset_email_invalid',
'A valid user email is required',
)
}
return normalized
}
function expirySeconds(value = defaultExpirySeconds): number {
if (!Number.isInteger(value) || value < 300 || value > 3_600) {
throw new DomainError(
'password_reset_expiry_invalid',
'Password reset expiry must be between 5 and 60 minutes',
)
}
return value
}
export async function issueOperatorPasswordResetToken(
dependencies: PasswordResetDependencies,
request: IssueOperatorPasswordResetRequest,
): Promise<IssuedPasswordReset> {
const now = dependencies.now?.() ?? new Date()
const rawToken = (
dependencies.generateToken ?? (() => randomBytes(32).toString('base64url'))
)()
const tokenHash = dependencies.digester.digest(rawToken)
const expiresAt = new Date(
now.getTime() + expirySeconds(request.expiresInSeconds) * 1_000,
)
const publicBaseUrl = new URL(request.publicBaseUrl)
if (
publicBaseUrl.protocol !== 'https:' &&
publicBaseUrl.protocol !== 'http:'
) {
throw new DomainError(
'password_reset_public_url_invalid',
'Password reset public URL must use HTTP or HTTPS',
)
}
if (publicBaseUrl.username || publicBaseUrl.password) {
throw new DomainError(
'password_reset_public_url_invalid',
'Password reset public URL must not contain credentials',
)
}
const resetUrl = new URL('/reset-password', publicBaseUrl)
resetUrl.hash = new URLSearchParams({ token: rawToken }).toString()
await dependencies.store.transaction(async (transaction) => {
const user = await transaction.findActiveUserByEmail(
normalizedEmail(request.email),
)
if (!user) {
throw new DomainError(
'password_reset_user_unavailable',
'No active user is available for password reset',
)
}
const revokedPriorTokens = await transaction.revokeUnusedTokens(
user.id,
now,
)
const token = await transaction.createToken({
userId: user.id,
tokenHash,
expiresAt,
createdBy: request.createdBy ?? null,
createdAt: now,
})
await transaction.appendAuditEvent({
actorUserId: request.createdBy ?? null,
action: 'user.password_reset.issued',
resourceId: user.id,
metadata: {
channel: request.createdBy ? 'administrator' : 'operator',
tokenId: token.id,
revokedPriorTokens,
expiresInSeconds: expirySeconds(request.expiresInSeconds),
},
})
})
return Object.freeze({ resetUrl: resetUrl.toString(), expiresAt })
}
export async function consumePasswordResetToken(
dependencies: PasswordResetDependencies,
request: ConsumePasswordResetRequest,
): Promise<void> {
if (request.betterAuthPasswordHash.trim().length === 0) {
throw new DomainError(
'password_reset_hash_missing',
'A local-identity password hash is required',
)
}
const now = dependencies.now?.() ?? new Date()
const tokenHash = dependencies.digester.digest(request.rawToken)
await dependencies.store.transaction(async (transaction) => {
const token = await transaction.consumeValidToken({
tokenHash,
consumedAt: now,
})
if (!token) {
throw new DomainError(
'password_reset_token_invalid',
'Password reset token is invalid, expired or already used',
)
}
if (
!(await transaction.updatePassword({
userId: token.userId,
passwordHash: request.betterAuthPasswordHash,
changedAt: now,
}))
) {
throw new DomainError(
'password_reset_user_unavailable',
'No active user is available for password reset',
)
}
const revokedSessions = await transaction.revokeSessions(token.userId, now)
await transaction.appendAuditEvent({
actorUserId: token.userId,
action: 'user.password_reset.completed',
resourceId: token.userId,
metadata: { tokenId: token.id, revokedSessions },
})
})
}
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest'
import { isSessionActive, resolveSessionDeadlines } from './session-policy'
describe('session policy', () => {
it('uses a 12-hour idle and 30-day absolute deadline', () => {
const createdAt = new Date('2026-01-01T00:00:00Z')
const deadlines = resolveSessionDeadlines({ createdAt, now: createdAt })
expect(deadlines.idleExpiresAt.toISOString()).toBe(
'2026-01-01T12:00:00.000Z',
)
expect(deadlines.absoluteExpiresAt.toISOString()).toBe(
'2026-01-31T00:00:00.000Z',
)
expect(isSessionActive(createdAt, deadlines)).toBe(true)
})
it('never refreshes idle expiry beyond absolute expiry', () => {
const absoluteExpiresAt = new Date('2026-01-02T00:00:00Z')
const deadlines = resolveSessionDeadlines({
createdAt: new Date('2026-01-01T00:00:00Z'),
now: new Date('2026-01-01T23:00:00Z'),
absoluteExpiresAt,
})
expect(deadlines.idleExpiresAt).toEqual(absoluteExpiresAt)
expect(
isSessionActive(absoluteExpiresAt, { ...deadlines, revokedAt: null }),
).toBe(false)
})
})
@@ -0,0 +1,40 @@
export interface SessionDeadlineInput {
createdAt: Date
now: Date
absoluteExpiresAt?: Date
idleSeconds?: number
absoluteSeconds?: number
}
export interface SessionDeadlines {
idleExpiresAt: Date
absoluteExpiresAt: Date
}
export function resolveSessionDeadlines({
createdAt,
now,
absoluteExpiresAt,
idleSeconds = 12 * 60 * 60,
absoluteSeconds = 30 * 24 * 60 * 60,
}: SessionDeadlineInput): SessionDeadlines {
const absolute =
absoluteExpiresAt ?? new Date(createdAt.getTime() + absoluteSeconds * 1_000)
const proposedIdle = new Date(now.getTime() + idleSeconds * 1_000)
return {
idleExpiresAt:
proposedIdle.getTime() < absolute.getTime() ? proposedIdle : absolute,
absoluteExpiresAt: absolute,
}
}
export function isSessionActive(
now: Date,
session: SessionDeadlines & { revokedAt?: Date | null },
): boolean {
return (
!session.revokedAt &&
now.getTime() < session.idleExpiresAt.getTime() &&
now.getTime() < session.absoluteExpiresAt.getTime()
)
}
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { TokenDigester } from './token-digest'
describe('TokenDigester', () => {
it('creates stable non-bearer digests and compares in constant-time primitives', () => {
const digester = new TokenDigester(Buffer.alloc(32, 3))
const token = 'a'.repeat(32)
const digest = digester.digest(token)
expect(digest).not.toContain(token)
expect(digester.matches(token, digest)).toBe(true)
expect(digester.matches('b'.repeat(32), digest)).toBe(false)
})
it('rejects weak pepper and low-entropy tokens', () => {
expect(() => new TokenDigester(Buffer.alloc(16))).toThrow(
/at least 32 bytes/,
)
expect(() => new TokenDigester(Buffer.alloc(32)).digest('short')).toThrow(
/low-entropy/,
)
})
})
@@ -0,0 +1,26 @@
import { createHmac, timingSafeEqual } from 'node:crypto'
const digestPrefix = 'hmac-sha256:v1:'
export class TokenDigester {
constructor(private readonly pepper: Buffer) {
if (pepper.byteLength < 32) {
throw new Error('Token digest pepper must contain at least 32 bytes')
}
}
digest(token: string): string {
if (token.length < 16)
throw new Error('Refusing to digest a low-entropy token')
return `${digestPrefix}${createHmac('sha256', this.pepper).update(token, 'utf8').digest('hex')}`
}
matches(token: string, storedDigest: string): boolean {
const candidate = Buffer.from(this.digest(token), 'utf8')
const stored = Buffer.from(storedDigest, 'utf8')
return (
candidate.byteLength === stored.byteLength &&
timingSafeEqual(candidate, stored)
)
}
}
@@ -0,0 +1,145 @@
import { describe, expect, it } from 'vitest'
import {
authorizeWorkspaceAction,
type WorkspaceAuthorizationLookup,
type WorkspaceAuthorizationRecord,
type WorkspaceRole,
} from './workspace-authorization'
const userId = '00000000-0000-4000-8000-000000000001'
const workspaceId = '00000000-0000-4000-8000-000000000002'
function record(
workspaceRole: WorkspaceRole,
overrides: Partial<WorkspaceAuthorizationRecord> = {},
): WorkspaceAuthorizationRecord {
return {
userId,
instanceRole: 'user',
workspaceId,
workspaceRole,
userStatus: 'active',
...overrides,
}
}
class MemoryAuthorizationLookup implements WorkspaceAuthorizationLookup {
calls: Array<{ userId: string; workspaceId: string }> = []
constructor(readonly authorization: WorkspaceAuthorizationRecord | null) {}
async findWorkspaceAuthorization(user: string, workspace: string) {
this.calls.push({ userId: user, workspaceId: workspace })
return this.authorization
}
}
async function authorize(
lookup: WorkspaceAuthorizationLookup,
action: 'read' | 'write' | 'destructive',
) {
return authorizeWorkspaceAction(lookup, {
actor: { userId },
workspaceId,
action,
})
}
function expectGenericDenial(promise: Promise<unknown>) {
return expect(promise).rejects.toMatchObject({
code: 'workspace_access_denied',
message: 'Workspace access is not permitted',
details: {},
})
}
describe('authorizeWorkspaceAction', () => {
it('rejects unauthenticated requests without querying workspace state', async () => {
const lookup = new MemoryAuthorizationLookup(record('owner'))
await expect(
authorizeWorkspaceAction(lookup, {
actor: null,
workspaceId,
action: 'read',
}),
).rejects.toMatchObject({ code: 'authentication_required' })
expect(lookup.calls).toEqual([])
})
it('uses the same denial for absent membership and cross-workspace substitution', async () => {
await expectGenericDenial(
authorize(new MemoryAuthorizationLookup(null), 'read'),
)
await expectGenericDenial(
authorize(
new MemoryAuthorizationLookup(
record('owner', {
workspaceId: '00000000-0000-4000-8000-000000000099',
}),
),
'read',
),
)
})
it('allows viewer reads but denies viewer mutation', async () => {
await expect(
authorize(new MemoryAuthorizationLookup(record('viewer')), 'read'),
).resolves.toMatchObject({
userId,
workspaceId,
workspaceRole: 'viewer',
})
await expectGenericDenial(
authorize(new MemoryAuthorizationLookup(record('viewer')), 'write'),
)
})
it('allows editor writes but not destructive actions', async () => {
await expect(
authorize(new MemoryAuthorizationLookup(record('editor')), 'write'),
).resolves.toMatchObject({ workspaceRole: 'editor' })
await expectGenericDenial(
authorize(new MemoryAuthorizationLookup(record('editor')), 'destructive'),
)
})
it('allows owner destructive actions and returns a frozen application context', async () => {
const context = await authorize(
new MemoryAuthorizationLookup(
record('owner', { instanceRole: 'instance_owner' }),
),
'destructive',
)
expect(context).toEqual({
userId,
instanceRole: 'instance_owner',
workspaceId,
workspaceRole: 'owner',
})
expect(Object.isFrozen(context)).toBe(true)
})
it('does not grant an instance admin access without membership', async () => {
// A lookup returns null when the user has no membership; instance role is
// deliberately unavailable and cannot be used as an authorization bypass.
await expectGenericDenial(
authorize(new MemoryAuthorizationLookup(null), 'read'),
)
})
it.each(['disabled', 'pending_deletion'] as const)(
'denies %s users with the generic workspace error',
async (userStatus) => {
await expectGenericDenial(
authorize(
new MemoryAuthorizationLookup(record('owner', { userStatus })),
'read',
),
)
},
)
})
@@ -0,0 +1,122 @@
import { DomainError } from '@devrunbook/domain'
export const instanceRoles = [
'instance_owner',
'instance_admin',
'user',
] as const
export type InstanceRole = (typeof instanceRoles)[number]
export const workspaceRoles = ['viewer', 'editor', 'owner'] as const
export type WorkspaceRole = (typeof workspaceRoles)[number]
export const workspaceActions = ['read', 'write', 'destructive'] as const
export type WorkspaceAction = (typeof workspaceActions)[number]
export interface AuthenticatedActor {
readonly userId: string
}
export interface ActorContext {
readonly userId: string
readonly instanceRole: InstanceRole
readonly workspaceId: string
readonly workspaceRole: WorkspaceRole
}
export interface WorkspaceAuthorizationRecord extends ActorContext {
readonly userStatus: 'active' | 'disabled' | 'pending_deletion'
}
/**
* Implementations must resolve the actor and target workspace in one bounded
* lookup. Missing users, deleted users/workspaces, and missing memberships all
* return null so callers cannot distinguish cross-workspace object existence.
*/
export interface WorkspaceAuthorizationLookup {
findWorkspaceAuthorization(
userId: string,
workspaceId: string,
): Promise<WorkspaceAuthorizationRecord | null>
}
export interface ActiveWorkspaceLookup {
/** Returns one stable active membership without exposing other workspaces. */
findDeterministicActiveWorkspaceId(userId: string): Promise<string | null>
}
export interface WorkspaceSelectionOption {
readonly id: string
readonly name: string
readonly type: 'personal' | 'team'
readonly role: WorkspaceRole
}
export interface WorkspaceSelectionLookup {
/** Lists only active, authorized memberships for the signed-in actor. */
listAuthorizedWorkspaces(
userId: string,
): Promise<readonly WorkspaceSelectionOption[]>
}
export interface AuthorizeWorkspaceRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly action: WorkspaceAction
}
const minimumRole: Record<WorkspaceAction, WorkspaceRole> = {
read: 'viewer',
write: 'editor',
destructive: 'owner',
}
const roleRank: Record<WorkspaceRole, number> = {
viewer: 0,
editor: 1,
owner: 2,
}
function denyWorkspaceAccess(): never {
throw new DomainError(
'workspace_access_denied',
'Workspace access is not permitted',
)
}
export async function authorizeWorkspaceAction(
lookup: WorkspaceAuthorizationLookup,
request: AuthorizeWorkspaceRequest,
): Promise<ActorContext> {
if (!request.actor?.userId) {
throw new DomainError(
'authentication_required',
'Authentication is required',
)
}
const authorization = await lookup.findWorkspaceAuthorization(
request.actor.userId,
request.workspaceId,
)
if (
!authorization ||
!instanceRoles.includes(authorization.instanceRole) ||
!workspaceRoles.includes(authorization.workspaceRole) ||
!workspaceActions.includes(request.action) ||
authorization.userId !== request.actor.userId ||
authorization.workspaceId !== request.workspaceId ||
authorization.userStatus !== 'active' ||
roleRank[authorization.workspaceRole] <
roleRank[minimumRole[request.action]]
) {
denyWorkspaceAccess()
}
return Object.freeze({
userId: authorization.userId,
instanceRole: authorization.instanceRole,
workspaceId: authorization.workspaceId,
workspaceRole: authorization.workspaceRole,
})
}
@@ -0,0 +1,245 @@
import { describe, expect, it, vi } from 'vitest'
import type {
GeneratedRun,
GeneratedRunStore,
} from '../generated-runs/create-generated-run'
import {
generateAuthoritativeComposition,
previewAuthoritativeComposition,
type AuthoritativeCompositionDependencies,
type CompositionPlaybookVersion,
} from './authoritative-composition'
import { generateCompositionFromDraft } from './generate-composition-from-draft'
const actor = { userId: 'user-1' }
const workspaceId = 'workspace-1'
function playbook(
overrides: Partial<CompositionPlaybookVersion> = {},
): CompositionPlaybookVersion {
return {
id: 'version-1',
slug: 'safe-change',
version: '1.0.0',
digest: 'a'.repeat(64),
lifecycle: 'validated',
manifest: {
metadata: {
slug: 'safe-change',
version: '1.0.0',
title: 'Safe change',
},
spec: {
intent: { outcome: 'Make a bounded, verified change.' },
modes: ['plan'],
autonomy: { min: 'plan', max: 'verify', default: 'plan' },
inputs: [
{
key: 'request',
type: 'multiline',
required: true,
includeInOutput: true,
},
],
guardrails: [{ id: 'bounded', text: 'Keep the change bounded.' }],
workflow: [
{
id: 'inspect',
title: 'Inspect',
instruction: 'Inspect before changing anything.',
},
],
validation: {
checks: [
{
id: 'verify',
description: 'Verify the result.',
evidence: 'Report the relevant check.',
},
],
},
completion: { criteria: ['The requested change is verified.'] },
reporting: {
sections: [
{
title: 'Summary',
description: 'Report the change and verification evidence.',
},
],
},
},
},
template: '# Request\n\n{{ inputs.request }}\n',
...overrides,
}
}
function dependencies(
role: 'viewer' | 'editor' | 'owner' = 'editor',
version = playbook(),
): AuthoritativeCompositionDependencies {
return {
authorization: {
findWorkspaceAuthorization: vi.fn(async () => ({
userId: actor.userId,
workspaceId,
instanceRole: 'user' as const,
workspaceRole: role,
userStatus: 'active' as const,
})),
},
sources: {
findPublishedPlaybookVersion: vi.fn(async () => version),
findPublishedPlaybookVersionById: vi.fn(async () => version),
findRepositoryProfileRevision: vi.fn(async () => null),
},
}
}
const request = {
actor,
workspaceId,
playbook: { slug: 'safe-change', version: '1.0.0' },
inputs: { request: 'Update the documented behavior.' },
workMode: 'plan',
autonomyLevel: 'plan' as const,
}
describe('authoritative composition', () => {
it('loads immutable sources and produces stable server-owned snapshots', async () => {
const first = await previewAuthoritativeComposition(dependencies(), request)
const second = await previewAuthoritativeComposition(
dependencies(),
request,
)
expect(first.preview.renderDigest).toBe(second.preview.renderDigest)
expect(first.preview.renderedPrompt).toBe(second.preview.renderedPrompt)
expect(first.preview.lintFindings).toEqual([
expect.objectContaining({ ruleId: 'PB006', severity: 'warning' }),
])
expect(first.snapshots.playbook).toMatchObject({
id: 'version-1',
digest: 'a'.repeat(64),
})
expect(first.snapshots.normalizedInput).toEqual({
request: 'Update the documented behavior.',
})
expect(first.snapshots.policy).toMatchObject({ outputFormat: 'prompt' })
})
it('generates from server-computed prompt, lint and actor identity', async () => {
let stored: GeneratedRun | undefined
const store: GeneratedRunStore = {
createIdempotently: vi.fn(async (candidate) => {
stored = candidate
return { run: candidate, created: true }
}),
}
const result = await generateAuthoritativeComposition(
{
...dependencies(),
store,
nextId: () => 'run-1',
now: () => new Date('2026-07-27T13:00:00.000Z'),
},
{ ...request, idempotencyKey: 'request-1' },
)
expect(result.created).toBe(true)
expect(stored).toMatchObject({
generatedBy: actor.userId,
playbookVersionId: 'version-1',
lint: { exportReadiness: 'warning' },
})
expect(stored?.renderedPrompt).toContain('Update the documented behavior.')
})
it('blocks missing required input before immutable persistence', async () => {
const store: GeneratedRunStore = {
createIdempotently: vi.fn(),
}
await expect(
generateAuthoritativeComposition(
{
...dependencies(),
store,
nextId: () => 'run-1',
now: () => new Date(),
},
{ ...request, inputs: {}, idempotencyKey: 'request-1' },
),
).rejects.toMatchObject({ code: 'generated_run_lint_blocked' })
expect(store.createIdempotently).not.toHaveBeenCalled()
})
it('rejects viewers and mismatched persisted playbook identity', async () => {
await expect(
previewAuthoritativeComposition(dependencies('viewer'), request),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
previewAuthoritativeComposition(
dependencies('editor', playbook({ slug: 'different' })),
request,
),
).rejects.toMatchObject({ code: 'composition_playbook_integrity_failed' })
})
it('freezes server-loaded draft state without accepting client composition data', async () => {
let stored: GeneratedRun | undefined
const base = dependencies()
const result = await generateCompositionFromDraft(
{
...base,
drafts: {
create: vi.fn(),
patchWithRevision: vi.fn(),
findByIdForWorkspace: vi.fn(async () => ({
id: 'draft-1',
workspaceId,
playbookVersionId: 'version-1',
repositoryProfileRevisionId: null,
inputs: { request: 'The persisted draft is authoritative.' },
scopeOverrides: {},
policyOverrides: {},
autonomyLevel: 'plan' as const,
workMode: 'plan' as const,
outputFormat: 'prompt' as const,
lastRenderDigest: null,
revision: 3,
createdBy: actor.userId,
createdAt: '2026-07-27T12:00:00.000Z',
updatedAt: '2026-07-27T12:05:00.000Z',
})),
},
store: {
createIdempotently: vi.fn(async (candidate) => {
stored = candidate
return { run: candidate, created: true }
}),
},
nextId: () => 'run-1',
now: () => new Date('2026-07-27T13:00:00.000Z'),
},
{
actor,
workspaceId,
draftId: 'draft-1',
idempotencyKey: 'draft-generation-1',
},
)
expect(result.created).toBe(true)
expect(stored).toMatchObject({
sourceDraftId: 'draft-1',
playbookVersionId: 'version-1',
snapshots: {
normalizedInput: {
request: 'The persisted draft is authoritative.',
},
},
})
})
})
@@ -0,0 +1,327 @@
import {
composePreview,
type ComposePreviewRequest,
type ComposePreviewResult,
type PlaybookMetadata,
type PlaybookSpecification,
} from '@devrunbook/composer'
import { DomainError, type AutonomyLevel } from '@devrunbook/domain'
import type { RepositoryProfile } from '@devrunbook/repository-intel'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import {
createGeneratedRun,
type GeneratedRunCreationDependencies,
type GeneratedRunLintResult,
type GeneratedRunSnapshots,
type ImmutableJsonObject,
type ImmutableJsonValue,
type StoreGeneratedRunResult,
} from '../generated-runs/create-generated-run'
export interface CompositionPlaybookVersion {
readonly id: string
readonly slug: string
readonly version: string
readonly digest: string
readonly lifecycle: string
readonly manifest: Readonly<Record<string, unknown>>
readonly template: string
}
export interface CompositionRepositoryProfileRevision {
readonly id: string
readonly repositoryId: string
readonly revisionNumber: number
readonly contentDigest: string
readonly profile: RepositoryProfile
}
export interface CompositionSourceReader {
findPublishedPlaybookVersion(
workspaceId: string,
slug: string,
version: string,
): Promise<CompositionPlaybookVersion | null>
findPublishedPlaybookVersionById(
workspaceId: string,
versionId: string,
): Promise<CompositionPlaybookVersion | null>
findRepositoryProfileRevision(
workspaceId: string,
revisionId: string,
): Promise<CompositionRepositoryProfileRevision | null>
}
export interface AuthoritativeCompositionDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly sources: CompositionSourceReader
}
export interface AuthoritativeCompositionRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly playbook: {
readonly slug: string
readonly version: string
}
readonly repositoryProfileRevisionId?: string | null
readonly inputs: Readonly<Record<string, unknown>>
readonly scopeOverrides?: ComposePreviewRequest['scopeOverrides']
readonly workMode: string
readonly autonomyLevel: AutonomyLevel
readonly outputFormat?: 'prompt' | 'markdown' | 'run-pack'
readonly confirmedUnsafeCommandIds?: readonly string[]
}
export interface AuthoritativeCompositionResult {
readonly playbookVersionId: string
readonly repositoryProfileRevisionId: string | null
readonly preview: ComposePreviewResult
readonly snapshots: GeneratedRunSnapshots
}
export interface GenerateAuthoritativeCompositionRequest extends AuthoritativeCompositionRequest {
readonly sourceDraftId?: string | null
readonly idempotencyKey: string
}
function plainObject(
value: unknown,
): value is Readonly<Record<string, unknown>> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function immutableJson(value: unknown, path: string): ImmutableJsonValue {
if (value === null || typeof value === 'string' || typeof value === 'boolean')
return value
if (typeof value === 'number' && Number.isFinite(value)) return value
if (Array.isArray(value))
return value.map((item, index) => immutableJson(item, `${path}[${index}]`))
if (plainObject(value))
return Object.fromEntries(
Object.entries(value)
.filter(([, item]) => item !== undefined)
.sort(([left], [right]) => left.localeCompare(right, 'en'))
.map(([key, item]) => [key, immutableJson(item, `${path}.${key}`)]),
)
throw new DomainError(
'composition_source_invalid',
'An immutable composition source contains invalid JSON data',
{ path },
)
}
function immutableJsonObject(
value: unknown,
path: string,
): ImmutableJsonObject {
const normalized = immutableJson(value, path)
if (
normalized === null ||
Array.isArray(normalized) ||
typeof normalized !== 'object'
)
throw new DomainError(
'composition_source_invalid',
'An immutable composition source must contain a JSON object',
{ path },
)
return normalized as ImmutableJsonObject
}
function manifestContract(version: CompositionPlaybookVersion): {
readonly metadata: PlaybookMetadata
readonly specification: PlaybookSpecification
} {
const metadata = version.manifest.metadata
const specification = version.manifest.spec
if (
!plainObject(metadata) ||
typeof metadata.slug !== 'string' ||
typeof metadata.version !== 'string' ||
typeof metadata.title !== 'string' ||
metadata.slug !== version.slug ||
metadata.version !== version.version ||
!plainObject(specification) ||
!plainObject(specification.intent) ||
typeof specification.intent.outcome !== 'string'
) {
throw new DomainError(
'composition_playbook_integrity_failed',
'Published playbook metadata does not match its immutable version',
)
}
return {
metadata: {
slug: metadata.slug,
version: metadata.version,
title: metadata.title,
},
specification: specification as unknown as PlaybookSpecification,
}
}
async function resolveComposition(
dependencies: AuthoritativeCompositionDependencies,
request: AuthoritativeCompositionRequest,
): Promise<AuthoritativeCompositionResult> {
await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'write',
})
const version = await dependencies.sources.findPublishedPlaybookVersion(
request.workspaceId,
request.playbook.slug,
request.playbook.version,
)
if (!version)
throw new DomainError(
'composition_source_not_found',
'Composition source not found',
)
if (
version.slug !== request.playbook.slug ||
version.version !== request.playbook.version ||
!/^[0-9a-f]{64}$/u.test(version.digest)
)
throw new DomainError(
'composition_playbook_integrity_failed',
'Published playbook metadata does not match its immutable version',
)
const revision = request.repositoryProfileRevisionId
? await dependencies.sources.findRepositoryProfileRevision(
request.workspaceId,
request.repositoryProfileRevisionId,
)
: null
if (request.repositoryProfileRevisionId && !revision)
throw new DomainError(
'composition_source_not_found',
'Composition source not found',
)
if (
revision &&
(revision.id !== request.repositoryProfileRevisionId ||
revision.revisionNumber !== revision.profile.metadata.revision ||
revision.contentDigest !== revision.profile.metadata.contentDigest ||
!/^[0-9a-f]{64}$/u.test(revision.contentDigest))
)
throw new DomainError(
'composition_repository_integrity_failed',
'Repository profile metadata does not match its immutable revision',
)
const contract = manifestContract(version)
const preview = composePreview({
metadata: contract.metadata,
specification: contract.specification,
template: version.template,
inputs: request.inputs,
workMode: request.workMode,
autonomyLevel: request.autonomyLevel,
repositoryProfile: revision?.profile ?? null,
...(request.scopeOverrides
? { scopeOverrides: request.scopeOverrides }
: {}),
...(request.confirmedUnsafeCommandIds
? { confirmedUnsafeCommandIds: request.confirmedUnsafeCommandIds }
: {}),
})
const snapshots: GeneratedRunSnapshots = {
playbook: immutableJsonObject(
{
id: version.id,
slug: version.slug,
version: version.version,
digest: version.digest,
lifecycle: version.lifecycle,
manifest: version.manifest,
template: version.template,
},
'playbook',
),
repositoryProfile: revision
? immutableJsonObject(
{
revisionId: revision.id,
repositoryId: revision.repositoryId,
revisionNumber: revision.revisionNumber,
contentDigest: revision.contentDigest,
profile: revision.profile,
},
'repositoryProfile',
)
: null,
normalizedInput: immutableJsonObject(
preview.normalizedInput,
'normalizedInput',
),
policy: immutableJsonObject(
{
workMode: request.workMode,
autonomyLevel: request.autonomyLevel,
outputFormat: request.outputFormat ?? 'prompt',
compatibility: preview.compatibility,
resolvedPolicies: preview.resolvedPolicies,
resolvedScope: preview.resolvedScope,
},
'policy',
),
provenance: immutableJson(
[
...preview.provenance.map((item) => ({
kind: 'block',
...item,
})),
...preview.conditionAccesses.map((item) => ({
kind: 'condition-fact',
...item,
})),
],
'provenance',
) as readonly ImmutableJsonValue[],
}
return {
playbookVersionId: version.id,
repositoryProfileRevisionId: revision?.id ?? null,
preview,
snapshots,
}
}
export function previewAuthoritativeComposition(
dependencies: AuthoritativeCompositionDependencies,
request: AuthoritativeCompositionRequest,
): Promise<AuthoritativeCompositionResult> {
return resolveComposition(dependencies, request)
}
export async function generateAuthoritativeComposition(
dependencies: AuthoritativeCompositionDependencies &
GeneratedRunCreationDependencies,
request: GenerateAuthoritativeCompositionRequest,
): Promise<StoreGeneratedRunResult> {
const resolved = await resolveComposition(dependencies, request)
const lint: GeneratedRunLintResult = {
exportReadiness: resolved.preview.exportReadiness,
findings: resolved.preview.lintFindings,
}
return createGeneratedRun(dependencies, {
workspaceId: request.workspaceId,
generatedBy: request.actor!.userId,
sourceDraftId: request.sourceDraftId ?? null,
playbookVersionId: resolved.playbookVersionId,
snapshots: resolved.snapshots,
lint,
renderedPrompt: resolved.preview.renderedPrompt,
renderDigest: resolved.preview.renderDigest,
idempotencyKey: request.idempotencyKey,
})
}
@@ -0,0 +1,226 @@
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import type {
CanonicalPromptRequest,
PlaybookMetadata,
PlaybookSpecification,
RepositoryProfile,
} from '@devrunbook/composer'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import { composeAndCreateGeneratedRun } from './compose-and-create-generated-run'
import {
computeRenderDigest,
type GeneratedRun,
type GeneratedRunStore,
type ImmutableJsonObject,
} from '../generated-runs/create-generated-run'
class RecordingStore implements GeneratedRunStore {
candidate: GeneratedRun | undefined
async createIdempotently(candidate: GeneratedRun) {
this.candidate = candidate
return { run: candidate, created: true }
}
}
const snapshots = {
playbook: { slug: 'bounded-change', version: '1.0.0' },
repositoryProfile: null,
normalizedInput: { request: 'Implement the bounded change' },
policy: { autonomyLevel: 'verify', conditionsResolved: true },
provenance: [{ block: 'mission', source: 'playbook' }],
} as const
function dependencies(store: GeneratedRunStore) {
return {
store,
nextId: () => 'run-1',
now: () => new Date('2026-07-27T12:00:00.000Z'),
workspaceAuthorization: {
findWorkspaceAuthorization: async (
userId: string,
workspaceId: string,
) => ({
userId,
workspaceId,
instanceRole: 'user' as const,
workspaceRole: 'editor' as const,
userStatus: 'active' as const,
}),
},
}
}
describe('composeAndCreateGeneratedRun', () => {
it('renders once and immediately persists the exact bytes and verified digest', async () => {
const store = new RecordingStore()
const result = await composeAndCreateGeneratedRun(dependencies(store), {
prompt: {
metadata: {
slug: 'bounded-change',
version: '1.0.0',
title: 'Bounded change',
},
specification: {
intent: { outcome: 'Implement only the requested change.' },
guardrails: [{ text: 'Do not broaden scope.' }],
workflow: [
{
title: 'Implement',
instruction: 'Make the smallest coherent change.',
},
],
completion: { criteria: ['Targeted validation passes.'] },
},
template: '# Context\n\nRequest: {{ inputs.request }}',
inputs: { request: 'Implement the bounded change' },
workMode: 'execute',
autonomyLevel: 'verify',
},
snapshots,
lint: { exportReadiness: 'ready', findings: [] },
generatedBy: 'user-1',
workspaceId: 'workspace-1',
playbookVersionId: 'playbook-version-1',
idempotencyKey: 'compose-1',
})
expect(result.created).toBe(true)
expect(store.candidate).toStrictEqual(result.run)
expect(result.run.renderedPrompt).toContain(
'Request: Implement the bounded change',
)
expect(result.run.renderDigest).toBe(
computeRenderDigest(result.run.renderedPrompt),
)
expect(Object.isFrozen(result.run.snapshots.policy)).toBe(true)
})
it('loads the root-cause inputs and persists golden prompt bytes unchanged', async () => {
const repositoryRoot = path.resolve(import.meta.dirname, '../../../..')
const playbookRoot = path.join(
repositoryRoot,
'content/playbooks/root-cause-bugfix',
)
const playbook = parse(
await readFile(path.join(playbookRoot, 'playbook.yaml'), 'utf8'),
) as {
metadata: PlaybookMetadata
spec: PlaybookSpecification & { template: { main: string } }
}
const example = parse(
await readFile(path.join(playbookRoot, 'examples/minimal.yaml'), 'utf8'),
) as {
workMode: string
autonomyLevel: CanonicalPromptRequest['autonomyLevel']
inputs: CanonicalPromptRequest['inputs'] & ImmutableJsonObject
}
const profile = parse(
await readFile(
path.join(
repositoryRoot,
'examples/repository-profiles/example-profile.yaml',
),
'utf8',
),
) as RepositoryProfile
const golden = await readFile(
path.join(
repositoryRoot,
'examples/rendered-prompts/root-cause-bugfix.md',
),
'utf8',
)
const store = new RecordingStore()
const result = await composeAndCreateGeneratedRun(dependencies(store), {
prompt: {
metadata: playbook.metadata,
specification: playbook.spec,
template: await readFile(
path.join(playbookRoot, playbook.spec.template.main),
'utf8',
),
inputs: example.inputs,
workMode: example.workMode,
autonomyLevel: example.autonomyLevel,
repositoryProfile: profile,
},
snapshots: {
playbook: {
slug: playbook.metadata.slug,
version: playbook.metadata.version,
},
repositoryProfile: { name: profile.metadata.name, revision: 1 },
normalizedInput: example.inputs,
policy: { autonomyLevel: example.autonomyLevel },
provenance: [],
},
lint: { exportReadiness: 'ready', findings: [] },
generatedBy: 'fixture-user',
workspaceId: 'fixture-workspace',
playbookVersionId: 'fixture-playbook-version',
idempotencyKey: 'root-cause-golden',
})
expect(result.run.renderedPrompt).toBe(golden)
expect(store.candidate?.renderedPrompt).toBe(golden)
expect(result.run.renderDigest).toBe(computeRenderDigest(golden))
})
it('rejects invalid declared inputs, modes, autonomy and missing repositories before storage', async () => {
const store = new RecordingStore()
await expect(
composeAndCreateGeneratedRun(dependencies(store), {
prompt: {
metadata: { slug: 'strict', version: '1.0.0', title: 'Strict' },
specification: {
intent: { outcome: 'Validate first.' },
modes: ['execute'],
autonomy: { min: 'implement', max: 'verify', default: 'verify' },
inputs: [
{
key: 'request',
type: 'string',
required: true,
minLength: 3,
},
],
compatibility: { repositoryRequired: true },
},
template: '{{ inputs.request }}',
inputs: { request: '' },
workMode: 'inspect',
autonomyLevel: 'repair',
},
snapshots: {
...snapshots,
normalizedInput: { request: 'different' },
policy: { autonomyLevel: 'observe' },
},
lint: { exportReadiness: 'ready', findings: [] },
generatedBy: 'user-1',
workspaceId: 'workspace-1',
playbookVersionId: 'playbook-version-1',
idempotencyKey: 'strict-invalid',
}),
).rejects.toMatchObject({
code: 'composition_input_invalid',
details: {
issues: expect.arrayContaining([
'workMode is not supported by this playbook',
'autonomyLevel is outside the playbook autonomy range',
'repositoryProfile is required by this playbook',
'inputs.request must not be empty',
'snapshots.normalizedInput must match the composed inputs',
'snapshots.policy.autonomyLevel must match the composed autonomy level',
]),
},
})
expect(store.candidate).toBeUndefined()
})
})
@@ -0,0 +1,69 @@
import {
composeCanonicalPrompt,
type CanonicalPromptRequest,
} from '@devrunbook/composer'
import {
computeRenderDigest,
createGeneratedRun,
type GeneratedRunCreationDependencies,
type GeneratedRunLintResult,
type GeneratedRunSnapshots,
type StoreGeneratedRunResult,
} from '../generated-runs/create-generated-run'
import {
authorizeWorkspaceAction,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import { validateCompositionRequest } from './validate-composition-request'
export interface ComposeAndCreateGeneratedRunRequest {
/** Must already have conditions and policy precedence resolved upstream. */
readonly prompt: CanonicalPromptRequest
readonly snapshots: GeneratedRunSnapshots
readonly lint: GeneratedRunLintResult
readonly generatedBy: string
readonly workspaceId: string
readonly playbookVersionId: string
readonly sourceDraftId?: string | null
readonly idempotencyKey: string
}
export interface ComposeAndCreateGeneratedRunDependencies extends GeneratedRunCreationDependencies {
readonly workspaceAuthorization: WorkspaceAuthorizationLookup
}
/**
* The application boundary joining deterministic composition to immutable
* persistence. No condition evaluation, lint rewriting, or prompt mutation is
* allowed between rendering and digesting.
*/
export async function composeAndCreateGeneratedRun(
dependencies: ComposeAndCreateGeneratedRunDependencies,
request: ComposeAndCreateGeneratedRunRequest,
): Promise<StoreGeneratedRunResult> {
await authorizeWorkspaceAction(dependencies.workspaceAuthorization, {
actor: { userId: request.generatedBy },
workspaceId: request.workspaceId,
action: 'write',
})
validateCompositionRequest(request.prompt, request.snapshots)
const renderedPrompt = composeCanonicalPrompt(request.prompt)
const renderDigest = computeRenderDigest(renderedPrompt)
// createGeneratedRun independently recomputes and verifies this digest before
// invoking the store, preserving defense in depth at the persistence boundary.
return createGeneratedRun(dependencies, {
workspaceId: request.workspaceId,
generatedBy: request.generatedBy,
...(request.sourceDraftId === undefined
? {}
: { sourceDraftId: request.sourceDraftId }),
playbookVersionId: request.playbookVersionId,
snapshots: request.snapshots,
lint: request.lint,
renderedPrompt,
renderDigest,
idempotencyKey: request.idempotencyKey,
})
}
@@ -0,0 +1,241 @@
import { DomainError } from '@devrunbook/domain'
import { describe, expect, it, vi } from 'vitest'
import type {
WorkspaceAuthorizationRecord,
WorkspaceRole,
} from '../auth/workspace-authorization'
import {
createCompositionDraft,
formatCompositionDraftEtag,
getCompositionDraft,
parseCompositionDraftEtag,
patchCompositionDraft,
type CompositionDraft,
type CompositionDraftStore,
} from './composition-drafts'
const userId = '00000000-0000-4000-8000-000000000001'
const workspaceId = '00000000-0000-4000-8000-000000000002'
const draftId = '00000000-0000-4000-8000-000000000003'
function draft(overrides: Partial<CompositionDraft> = {}): CompositionDraft {
return {
id: draftId,
workspaceId,
playbookVersionId: '00000000-0000-4000-8000-000000000004',
repositoryProfileRevisionId: null,
inputs: { request: 'Make the bounded change' },
scopeOverrides: {},
policyOverrides: {},
autonomyLevel: 'verify',
workMode: 'execute',
outputFormat: 'prompt',
lastRenderDigest: null,
revision: 1,
createdBy: userId,
createdAt: '2026-07-27T12:00:00.000Z',
updatedAt: '2026-07-27T12:00:00.000Z',
...overrides,
}
}
class MemoryStore implements CompositionDraftStore {
readonly create = vi.fn<CompositionDraftStore['create']>(async () => draft())
readonly findByIdForWorkspace = vi.fn<
CompositionDraftStore['findByIdForWorkspace']
>(async () => draft())
readonly patchWithRevision = vi.fn<
CompositionDraftStore['patchWithRevision']
>(async () => ({ draft: draft({ revision: 2 }), changed: true }))
}
function authorization(role: WorkspaceRole): WorkspaceAuthorizationRecord {
return {
userId,
workspaceId,
workspaceRole: role,
instanceRole: 'user',
userStatus: 'active',
}
}
function dependencies(
role: WorkspaceRole = 'owner',
store = new MemoryStore(),
record: WorkspaceAuthorizationRecord | null = authorization(role),
) {
return {
store,
authorization: {
findWorkspaceAuthorization: vi.fn(async () => record),
},
}
}
const actor = { userId }
const createRequest = {
actor,
workspaceId,
playbookVersionId: '00000000-0000-4000-8000-000000000004',
inputs: { request: 'Make the bounded change' },
autonomyLevel: 'verify',
workMode: 'execute',
} as const
describe('composition draft authorization', () => {
it('rejects unauthenticated access before persistence', async () => {
const target = dependencies()
await expect(
getCompositionDraft(target, { actor: null, workspaceId, draftId }),
).rejects.toMatchObject({ code: 'authentication_required' })
expect(target.store.findByIdForWorkspace).not.toHaveBeenCalled()
})
it('conceals a missing membership from instance administrators', async () => {
const target = dependencies('owner', new MemoryStore(), null)
await expect(
getCompositionDraft(target, { actor, workspaceId, draftId }),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
})
it('allows viewers to read but denies autosave mutations', async () => {
const target = dependencies('viewer')
await expect(
getCompositionDraft(target, { actor, workspaceId, draftId }),
).resolves.toMatchObject({ etag: '"draft:1"' })
await expect(
createCompositionDraft(target, createRequest),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
patchCompositionDraft(target, {
actor,
workspaceId,
draftId,
expectedEtag: '"draft:1"',
patch: { inputs: {} },
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
})
it.each(['editor', 'owner'] as const)('allows %s writes', async (role) => {
const target = dependencies(role)
await expect(
createCompositionDraft(target, createRequest),
).resolves.toMatchObject({
etag: '"draft:1"',
})
await expect(
patchCompositionDraft(target, {
actor,
workspaceId,
draftId,
expectedEtag: '"draft:1"',
patch: { outputFormat: 'markdown' },
}),
).resolves.toMatchObject({ etag: '"draft:2"', changed: true })
})
})
describe('composition draft validation', () => {
it('normalizes defaults and freezes JSON before creation', async () => {
const target = dependencies('editor')
await createCompositionDraft(target, {
...createRequest,
inputs: { nested: { enabled: true } },
})
const request = target.store.create.mock.calls[0]![0]
expect(request).toMatchObject({
createdBy: userId,
repositoryProfileRevisionId: null,
scopeOverrides: {},
policyOverrides: {},
outputFormat: 'prompt',
lastRenderDigest: null,
})
expect(Object.isFrozen(request.inputs)).toBe(true)
expect(Object.isFrozen(request.inputs.nested)).toBe(true)
})
it('rejects non-JSON, invalid enums and digests before persistence', async () => {
const target = dependencies('editor')
await expect(
createCompositionDraft(target, {
...createRequest,
inputs: { invalid: Number.NaN },
}),
).rejects.toBeInstanceOf(DomainError)
await expect(
createCompositionDraft(target, {
...createRequest,
autonomyLevel: 'unbounded',
}),
).rejects.toMatchObject({ code: 'composition_draft_invalid' })
await expect(
createCompositionDraft(target, {
...createRequest,
lastRenderDigest: 'A'.repeat(64),
}),
).rejects.toMatchObject({ code: 'composition_draft_invalid' })
expect(target.store.create).not.toHaveBeenCalled()
})
it('passes only supplied patch members and parsed CAS revision', async () => {
const target = dependencies('editor')
await patchCompositionDraft(target, {
actor,
workspaceId,
draftId,
expectedEtag: '"draft:12"',
patch: { repositoryProfileRevisionId: null, inputs: { issue: 'fixed' } },
})
expect(target.store.patchWithRevision).toHaveBeenCalledWith({
workspaceId,
draftId,
expectedRevision: 12,
repositoryProfileRevisionId: null,
inputs: { issue: 'fixed' },
})
})
it('uses one safe not-found result for inaccessible references and IDs', async () => {
const createStore = new MemoryStore()
createStore.create.mockResolvedValue(null)
await expect(
createCompositionDraft(
dependencies('editor', createStore),
createRequest,
),
).rejects.toMatchObject({ code: 'composition_draft_not_found' })
const patchStore = new MemoryStore()
patchStore.patchWithRevision.mockResolvedValue(null)
await expect(
patchCompositionDraft(dependencies('editor', patchStore), {
actor,
workspaceId,
draftId,
expectedEtag: '"draft:1"',
patch: { inputs: {} },
}),
).rejects.toMatchObject({ code: 'composition_draft_not_found' })
})
})
describe('composition draft ETags', () => {
it('round-trips strong monotonic revisions', () => {
expect(formatCompositionDraftEtag(12)).toBe('"draft:12"')
expect(parseCompositionDraftEtag('"draft:12"')).toBe(12)
})
it.each(['', 'draft:1', 'W/"draft:1"', '"draft:0"', '"draft:01"'])(
'rejects malformed value %s',
(etag) => {
expect(() => parseCompositionDraftEtag(etag)).toThrowError(
expect.objectContaining({ code: 'composition_draft_etag_invalid' }),
)
},
)
})
@@ -0,0 +1,406 @@
import {
autonomyLevels,
DomainError,
type AutonomyLevel,
} from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
export const compositionWorkModes = [
'inspect',
'plan',
'guided',
'execute',
'recovery',
] as const
export type CompositionWorkMode = (typeof compositionWorkModes)[number]
export const compositionOutputFormats = [
'prompt',
'markdown',
'run-pack',
] as const
export type CompositionOutputFormat = (typeof compositionOutputFormats)[number]
export type CompositionJsonValue =
| null
| boolean
| number
| string
| readonly CompositionJsonValue[]
| CompositionJsonObject
export interface CompositionJsonObject {
readonly [key: string]: CompositionJsonValue
}
export interface CompositionDraft {
readonly id: string
readonly workspaceId: string
readonly playbookVersionId: string
readonly repositoryProfileRevisionId: string | null
readonly inputs: CompositionJsonObject
readonly scopeOverrides: CompositionJsonObject
readonly policyOverrides: CompositionJsonObject
readonly autonomyLevel: AutonomyLevel
readonly workMode: CompositionWorkMode
readonly outputFormat: CompositionOutputFormat
readonly lastRenderDigest: string | null
readonly revision: number
readonly createdBy: string
readonly createdAt: string
readonly updatedAt: string
}
export interface CreateCompositionDraftStoreRequest {
readonly workspaceId: string
readonly createdBy: string
readonly playbookVersionId: string
readonly repositoryProfileRevisionId: string | null
readonly inputs: CompositionJsonObject
readonly scopeOverrides: CompositionJsonObject
readonly policyOverrides: CompositionJsonObject
readonly autonomyLevel: AutonomyLevel
readonly workMode: CompositionWorkMode
readonly outputFormat: CompositionOutputFormat
readonly lastRenderDigest: string | null
}
export interface PatchCompositionDraftStoreRequest {
readonly workspaceId: string
readonly draftId: string
readonly expectedRevision: number
readonly repositoryProfileRevisionId?: string | null
readonly inputs?: CompositionJsonObject
readonly scopeOverrides?: CompositionJsonObject
readonly policyOverrides?: CompositionJsonObject
readonly autonomyLevel?: AutonomyLevel
readonly workMode?: CompositionWorkMode
readonly outputFormat?: CompositionOutputFormat
readonly lastRenderDigest?: string | null
}
export interface PatchCompositionDraftStoreResult {
readonly draft: CompositionDraft
readonly changed: boolean
}
export interface CompositionDraftStore {
/** Returns null when an immutable referenced resource is not accessible. */
create(
request: CreateCompositionDraftStoreRequest,
): Promise<CompositionDraft | null>
findByIdForWorkspace(
workspaceId: string,
draftId: string,
): Promise<CompositionDraft | null>
/**
* Applies the patch under a row lock. Missing and cross-workspace targets are
* concealed as null; stale revisions fail with composition_draft_conflict.
*/
patchWithRevision(
request: PatchCompositionDraftStoreRequest,
): Promise<PatchCompositionDraftStoreResult | null>
}
export interface CompositionDraftDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly store: CompositionDraftStore
}
export interface CompositionDraftActorRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
}
export interface CreateCompositionDraftRequest extends CompositionDraftActorRequest {
readonly playbookVersionId: string
readonly repositoryProfileRevisionId?: string | null
readonly inputs: unknown
readonly scopeOverrides?: unknown
readonly policyOverrides?: unknown
readonly autonomyLevel: string
readonly workMode: string
readonly outputFormat?: string
readonly lastRenderDigest?: string | null
}
export interface GetCompositionDraftRequest extends CompositionDraftActorRequest {
readonly draftId: string
}
export interface PatchCompositionDraftRequest extends GetCompositionDraftRequest {
readonly expectedEtag: string
readonly patch: {
readonly repositoryProfileRevisionId?: string | null
readonly inputs?: unknown
readonly scopeOverrides?: unknown
readonly policyOverrides?: unknown
readonly autonomyLevel?: string
readonly workMode?: string
readonly outputFormat?: string
readonly lastRenderDigest?: string | null
}
}
export interface CompositionDraftResult {
readonly draft: CompositionDraft
readonly etag: string
}
export interface PatchCompositionDraftResult extends CompositionDraftResult {
readonly changed: boolean
}
function draftNotFound(): never {
throw new DomainError(
'composition_draft_not_found',
'Composition draft not found',
)
}
function invalidDraft(issues: readonly string[]): never {
throw new DomainError(
'composition_draft_invalid',
'Composition draft is invalid',
{
issues,
},
)
}
function immutableJson(
value: unknown,
path: string,
depth = 0,
): CompositionJsonValue {
if (depth > 20) invalidDraft([`${path} exceeds the maximum nesting depth`])
if (
value === null ||
typeof value === 'boolean' ||
typeof value === 'string'
) {
return value
}
if (typeof value === 'number') {
if (!Number.isFinite(value))
invalidDraft([`${path} must contain finite numbers`])
return value
}
if (Array.isArray(value)) {
if (value.length > 100)
invalidDraft([`${path} must contain at most 100 items`])
return Object.freeze(
value.map((item, index) =>
immutableJson(item, `${path}/${index}`, depth + 1),
),
)
}
if (
typeof value !== 'object' ||
Object.getPrototypeOf(value) !== Object.prototype
) {
invalidDraft([`${path} must contain only JSON values`])
}
const entries = Object.entries(value as Record<string, unknown>)
if (entries.length > 100)
invalidDraft([`${path} must contain at most 100 properties`])
return Object.freeze(
Object.fromEntries(
entries.map(([key, item]) => {
if (key.length === 0 || key.length > 120) {
invalidDraft([`${path} contains an invalid property name`])
}
return [key, immutableJson(item, `${path}/${key}`, depth + 1)]
}),
),
)
}
function jsonObject(value: unknown, path: string): CompositionJsonObject {
const normalized = immutableJson(value, path)
if (
normalized === null ||
Array.isArray(normalized) ||
typeof normalized !== 'object'
) {
invalidDraft([`${path} must be an object`])
}
return normalized as CompositionJsonObject
}
function validDigest(value: string | null | undefined): string | null {
if (value === undefined || value === null) return null
if (!/^[a-f0-9]{64}$/u.test(value)) {
invalidDraft(['lastRenderDigest must be a lowercase SHA-256 digest'])
}
return value
}
function validAutonomy(value: string): AutonomyLevel {
if (!autonomyLevels.includes(value as AutonomyLevel)) {
invalidDraft(['autonomyLevel is invalid'])
}
return value as AutonomyLevel
}
function validWorkMode(value: string): CompositionWorkMode {
if (!compositionWorkModes.includes(value as CompositionWorkMode)) {
invalidDraft(['workMode is invalid'])
}
return value as CompositionWorkMode
}
function validOutputFormat(value: string | undefined): CompositionOutputFormat {
const format = value ?? 'prompt'
if (!compositionOutputFormats.includes(format as CompositionOutputFormat)) {
invalidDraft(['outputFormat is invalid'])
}
return format as CompositionOutputFormat
}
export function formatCompositionDraftEtag(revision: number): string {
if (!Number.isSafeInteger(revision) || revision < 1) {
throw new RangeError('Draft ETag revision must be a positive safe integer')
}
return `"draft:${revision}"`
}
export function parseCompositionDraftEtag(value: string): number {
const match = /^"draft:([1-9]\d*)"$/u.exec(value)
const revision = match ? Number(match[1]) : Number.NaN
if (!Number.isSafeInteger(revision)) {
throw new DomainError(
'composition_draft_etag_invalid',
'A valid current composition draft ETag is required',
)
}
return revision
}
async function authorize(
dependencies: CompositionDraftDependencies,
request: CompositionDraftActorRequest,
action: 'read' | 'write',
): Promise<string> {
const context = await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action,
})
return context.userId
}
export async function createCompositionDraft(
dependencies: CompositionDraftDependencies,
request: CreateCompositionDraftRequest,
): Promise<CompositionDraftResult> {
const createdBy = await authorize(dependencies, request, 'write')
const draft = await dependencies.store.create({
workspaceId: request.workspaceId,
createdBy,
playbookVersionId: request.playbookVersionId,
repositoryProfileRevisionId: request.repositoryProfileRevisionId ?? null,
inputs: jsonObject(request.inputs, '/inputs'),
scopeOverrides: jsonObject(request.scopeOverrides ?? {}, '/scopeOverrides'),
policyOverrides: jsonObject(
request.policyOverrides ?? {},
'/policyOverrides',
),
autonomyLevel: validAutonomy(request.autonomyLevel),
workMode: validWorkMode(request.workMode),
outputFormat: validOutputFormat(request.outputFormat),
lastRenderDigest: validDigest(request.lastRenderDigest),
})
if (!draft) draftNotFound()
return { draft, etag: formatCompositionDraftEtag(draft.revision) }
}
export async function getCompositionDraft(
dependencies: CompositionDraftDependencies,
request: GetCompositionDraftRequest,
): Promise<CompositionDraftResult> {
await authorize(dependencies, request, 'read')
const draft = await dependencies.store.findByIdForWorkspace(
request.workspaceId,
request.draftId,
)
if (!draft) draftNotFound()
return { draft, etag: formatCompositionDraftEtag(draft.revision) }
}
export async function patchCompositionDraft(
dependencies: CompositionDraftDependencies,
request: PatchCompositionDraftRequest,
): Promise<PatchCompositionDraftResult> {
await authorize(dependencies, request, 'write')
const patchKeys = Object.keys(request.patch)
const supportedPatchKeys = new Set([
'repositoryProfileRevisionId',
'inputs',
'scopeOverrides',
'policyOverrides',
'autonomyLevel',
'workMode',
'outputFormat',
'lastRenderDigest',
])
if (patchKeys.length === 0) {
invalidDraft(['patch must contain at least one property'])
}
if (patchKeys.some((key) => !supportedPatchKeys.has(key))) {
invalidDraft(['patch contains an unsupported property'])
}
const patch: PatchCompositionDraftStoreRequest = {
workspaceId: request.workspaceId,
draftId: request.draftId,
expectedRevision: parseCompositionDraftEtag(request.expectedEtag),
...('repositoryProfileRevisionId' in request.patch
? {
repositoryProfileRevisionId:
request.patch.repositoryProfileRevisionId,
}
: {}),
...('inputs' in request.patch
? { inputs: jsonObject(request.patch.inputs, '/inputs') }
: {}),
...('scopeOverrides' in request.patch
? {
scopeOverrides: jsonObject(
request.patch.scopeOverrides,
'/scopeOverrides',
),
}
: {}),
...('policyOverrides' in request.patch
? {
policyOverrides: jsonObject(
request.patch.policyOverrides,
'/policyOverrides',
),
}
: {}),
...(request.patch.autonomyLevel === undefined
? {}
: { autonomyLevel: validAutonomy(request.patch.autonomyLevel) }),
...(request.patch.workMode === undefined
? {}
: { workMode: validWorkMode(request.patch.workMode) }),
...(request.patch.outputFormat === undefined
? {}
: { outputFormat: validOutputFormat(request.patch.outputFormat) }),
...('lastRenderDigest' in request.patch
? { lastRenderDigest: validDigest(request.patch.lastRenderDigest) }
: {}),
}
const result = await dependencies.store.patchWithRevision(patch)
if (!result) draftNotFound()
return {
...result,
etag: formatCompositionDraftEtag(result.draft.revision),
}
}
@@ -0,0 +1,164 @@
import type { ScopeOverrides } from '@devrunbook/composer'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
} from '../auth/workspace-authorization'
import {
createGeneratedRun,
type GeneratedRunCreationDependencies,
type GeneratedRunLintResult,
type StoreGeneratedRunResult,
} from '../generated-runs/create-generated-run'
import {
previewAuthoritativeComposition,
type AuthoritativeCompositionDependencies,
} from './authoritative-composition'
import type {
CompositionDraftStore,
CompositionJsonObject,
} from './composition-drafts'
export interface GenerateCompositionFromDraftDependencies
extends
AuthoritativeCompositionDependencies,
GeneratedRunCreationDependencies {
readonly drafts: CompositionDraftStore
}
export interface GenerateCompositionFromDraftRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly draftId: string
readonly idempotencyKey: string
}
function stringArray(
value: unknown,
path: string,
): readonly string[] | undefined {
if (value === undefined) return undefined
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string'))
throw new DomainError(
'composition_draft_invalid',
'Composition draft is invalid',
{ issues: [`${path} must contain only strings`] },
)
return value
}
function scopeOverrides(value: CompositionJsonObject): ScopeOverrides {
const supported = new Set([
'includedPaths',
'excludedPaths',
'allowableChangeTypes',
'repositoryWideRead',
])
const unknown = Object.keys(value).filter((key) => !supported.has(key))
if (
unknown.length > 0 ||
(value.repositoryWideRead !== undefined &&
typeof value.repositoryWideRead !== 'boolean')
)
throw new DomainError(
'composition_draft_invalid',
'Composition draft is invalid',
{ issues: ['scopeOverrides contains unsupported values'] },
)
const includedPaths = stringArray(
value.includedPaths,
'scopeOverrides.includedPaths',
)
const excludedPaths = stringArray(
value.excludedPaths,
'scopeOverrides.excludedPaths',
)
const allowableChangeTypes = stringArray(
value.allowableChangeTypes,
'scopeOverrides.allowableChangeTypes',
)
return {
...(includedPaths ? { includedPaths } : {}),
...(excludedPaths ? { excludedPaths } : {}),
...(allowableChangeTypes ? { allowableChangeTypes } : {}),
...(typeof value.repositoryWideRead === 'boolean'
? { repositoryWideRead: value.repositoryWideRead }
: {}),
}
}
function assertNoPolicyOverride(value: CompositionJsonObject): void {
if (Object.keys(value).length > 0)
throw new DomainError(
'composition_draft_invalid',
'Composition draft is invalid',
{ issues: ['policyOverrides are not available in the MVP'] },
)
}
export async function generateCompositionFromDraft(
dependencies: GenerateCompositionFromDraftDependencies,
request: GenerateCompositionFromDraftRequest,
): Promise<StoreGeneratedRunResult> {
const actor = await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'write',
})
const draft = await dependencies.drafts.findByIdForWorkspace(
request.workspaceId,
request.draftId,
)
if (!draft)
throw new DomainError(
'composition_draft_not_found',
'Composition draft not found',
)
assertNoPolicyOverride(draft.policyOverrides)
const version = await dependencies.sources.findPublishedPlaybookVersionById(
request.workspaceId,
draft.playbookVersionId,
)
if (!version)
throw new DomainError(
'composition_source_not_found',
'Composition source not found',
)
const resolved = await previewAuthoritativeComposition(dependencies, {
actor: request.actor,
workspaceId: request.workspaceId,
playbook: { slug: version.slug, version: version.version },
repositoryProfileRevisionId: draft.repositoryProfileRevisionId,
inputs: draft.inputs,
scopeOverrides: scopeOverrides(draft.scopeOverrides),
workMode: draft.workMode,
autonomyLevel: draft.autonomyLevel,
outputFormat: draft.outputFormat,
})
if (
resolved.playbookVersionId !== draft.playbookVersionId ||
resolved.repositoryProfileRevisionId !== draft.repositoryProfileRevisionId
)
throw new DomainError(
'composition_source_changed',
'Composition sources changed while the immutable task was generated',
)
const lint: GeneratedRunLintResult = {
exportReadiness: resolved.preview.exportReadiness,
findings: resolved.preview.lintFindings,
}
return createGeneratedRun(dependencies, {
workspaceId: request.workspaceId,
generatedBy: actor.userId,
sourceDraftId: draft.id,
playbookVersionId: resolved.playbookVersionId,
snapshots: resolved.snapshots,
lint,
renderedPrompt: resolved.preview.renderedPrompt,
renderDigest: resolved.preview.renderDigest,
idempotencyKey: request.idempotencyKey,
})
}
@@ -0,0 +1,145 @@
import type { CanonicalPromptRequest } from '@devrunbook/composer'
import { DomainError, type AutonomyLevel } from '@devrunbook/domain'
import { isDeepStrictEqual } from 'node:util'
import type { GeneratedRunSnapshots } from '../generated-runs/create-generated-run'
const autonomyOrder: readonly AutonomyLevel[] = [
'observe',
'diagnose',
'plan',
'implement',
'verify',
'repair',
]
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string')
}
function inputIssue(
definition: NonNullable<
CanonicalPromptRequest['specification']['inputs']
>[number],
value: unknown,
): string | null {
const path = `inputs.${definition.key}`
if (value === undefined || value === null) {
return definition.required ? `${path} is required` : null
}
if (
['string', 'multiline', 'path', 'command', 'enum'].includes(definition.type)
) {
if (typeof value !== 'string') return `${path} must be a string`
if (definition.required && value.trim().length === 0)
return `${path} must not be empty`
if (
definition.minLength !== undefined &&
value.length < definition.minLength
)
return `${path} must contain at least ${definition.minLength} characters`
if (
definition.maxLength !== undefined &&
value.length > definition.maxLength
)
return `${path} must contain at most ${definition.maxLength} characters`
if (definition.type === 'enum' && !definition.options?.includes(value))
return `${path} must be one of the declared options`
return null
}
if (definition.type === 'boolean')
return typeof value === 'boolean' ? null : `${path} must be a boolean`
if (definition.type === 'integer') {
if (!Number.isInteger(value)) return `${path} must be an integer`
if (
definition.minimum !== undefined &&
(value as number) < definition.minimum
)
return `${path} must be at least ${definition.minimum}`
if (
definition.maximum !== undefined &&
(value as number) > definition.maximum
)
return `${path} must be at most ${definition.maximum}`
return null
}
if (!Array.isArray(value)) return `${path} must be an array`
if (definition.required && value.length === 0)
return `${path} must contain at least one item`
if (definition.type === 'string-list' || definition.type === 'multiselect') {
if (!isStringArray(value)) return `${path} must contain only strings`
if (
definition.type === 'multiselect' &&
value.some((item) => !definition.options?.includes(item))
)
return `${path} contains an undeclared option`
}
if (
definition.type === 'key-value-list' &&
value.some(
(item) =>
item === null ||
typeof item !== 'object' ||
Array.isArray(item) ||
Object.values(item).some((child) => typeof child !== 'string'),
)
)
return `${path} must contain string key-value objects`
return null
}
export function validateCompositionRequest(
prompt: CanonicalPromptRequest,
snapshots: GeneratedRunSnapshots,
): void {
const issues: string[] = []
const specification = prompt.specification
if (specification.modes && !specification.modes.includes(prompt.workMode))
issues.push('workMode is not supported by this playbook')
if (specification.autonomy) {
const selected = autonomyOrder.indexOf(prompt.autonomyLevel)
const minimum = autonomyOrder.indexOf(specification.autonomy.min)
const maximum = autonomyOrder.indexOf(specification.autonomy.max)
if (selected < minimum || selected > maximum)
issues.push('autonomyLevel is outside the playbook autonomy range')
}
if (
specification.compatibility?.repositoryRequired &&
!prompt.repositoryProfile
)
issues.push('repositoryProfile is required by this playbook')
const declaredInputs = new Set(
specification.inputs?.map((definition) => definition.key) ?? [],
)
for (const definition of specification.inputs ?? []) {
const issue = inputIssue(definition, prompt.inputs[definition.key])
if (issue) issues.push(issue)
}
for (const key of Object.keys(prompt.inputs)) {
if (specification.inputs && !declaredInputs.has(key))
issues.push(`inputs.${key} is not declared by this playbook`)
}
if (!isDeepStrictEqual(prompt.inputs, snapshots.normalizedInput))
issues.push('snapshots.normalizedInput must match the composed inputs')
if (snapshots.policy.autonomyLevel !== prompt.autonomyLevel)
issues.push(
'snapshots.policy.autonomyLevel must match the composed autonomy level',
)
if (issues.length > 0) {
throw new DomainError(
'composition_input_invalid',
'Composition input is invalid',
{ issues },
)
}
}
@@ -0,0 +1,168 @@
import { describe, expect, it } from 'vitest'
import {
computeRenderDigest,
createGeneratedRun,
type CreateGeneratedRunRequest,
type GeneratedRun,
type GeneratedRunStore,
type StoreGeneratedRunResult,
} from './create-generated-run'
class InMemoryGeneratedRunStore implements GeneratedRunStore {
readonly records = new Map<string, GeneratedRun>()
async createIdempotently(
candidate: GeneratedRun,
): Promise<StoreGeneratedRunResult> {
const key = `${candidate.workspaceId}:${candidate.idempotencyKey}`
const existing = this.records.get(key)
if (existing) return { run: existing, created: false }
this.records.set(key, candidate)
return { run: candidate, created: true }
}
}
const renderedPrompt = '# Task\n\nDo the bounded work.\n'
function request(
overrides: Partial<CreateGeneratedRunRequest> = {},
): CreateGeneratedRunRequest {
return {
workspaceId: 'workspace-1',
generatedBy: 'user-1',
sourceDraftId: 'draft-1',
playbookVersionId: 'playbook-version-1',
snapshots: {
playbook: { slug: 'root-cause-bugfix', version: '1.0.0' },
repositoryProfile: { revisionId: 'profile-revision-1' },
normalizedInput: { issue: 'The result is incorrect' },
policy: { autonomyLevel: 'verify' },
provenance: [{ block: 'mission', source: 'playbook' }],
},
lint: { exportReadiness: 'ready', findings: [] },
renderedPrompt,
renderDigest: computeRenderDigest(renderedPrompt),
idempotencyKey: 'generation-request-1',
...overrides,
}
}
function dependencies(
store: GeneratedRunStore = new InMemoryGeneratedRunStore(),
) {
let sequence = 0
return {
store,
nextId: () => `run-${++sequence}`,
now: () => new Date('2026-07-27T12:00:00.000Z'),
}
}
describe('createGeneratedRun', () => {
it('stores exact rendered bytes with verified digest and immutable snapshots', async () => {
const result = await createGeneratedRun(dependencies(), request())
expect(result.created).toBe(true)
expect(result.run).toMatchObject({
id: 'run-1',
renderedPrompt,
renderDigest: computeRenderDigest(renderedPrompt),
generatedAt: '2026-07-27T12:00:00.000Z',
})
expect(Object.isFrozen(result.run)).toBe(true)
expect(Object.isFrozen(result.run.snapshots.playbook)).toBe(true)
expect(Object.isFrozen(result.run.lint.findings)).toBe(true)
})
it('rejects a declared digest that does not match the exact prompt bytes', async () => {
await expect(
createGeneratedRun(
dependencies(),
request({ renderDigest: '0'.repeat(64) }),
),
).rejects.toMatchObject({ code: 'generated_run_render_digest_mismatch' })
})
it('rejects blocking lint findings before persistence', async () => {
const store = new InMemoryGeneratedRunStore()
await expect(
createGeneratedRun(
dependencies(store),
request({
lint: {
exportReadiness: 'blocked',
findings: [
{
ruleId: 'safety.protected-path',
severity: 'error',
message: 'Protected path is in modification scope',
source: 'scope',
},
],
},
}),
),
).rejects.toMatchObject({ code: 'generated_run_lint_blocked' })
expect(store.records.size).toBe(0)
})
it('returns the original immutable run for a repeated workspace key', async () => {
const store = new InMemoryGeneratedRunStore()
const deps = dependencies(store)
const first = await createGeneratedRun(deps, request())
const repeated = await createGeneratedRun(deps, request())
expect(first.created).toBe(true)
expect(repeated.created).toBe(false)
expect(repeated.run.id).toBe(first.run.id)
expect(repeated.run.generatedAt).toBe(first.run.generatedAt)
expect(store.records.size).toBe(1)
})
it.each([
['source draft', { sourceDraftId: 'draft-other' }],
['prompt bytes', { renderedPrompt: '# Different\n' }],
[
'snapshots',
{
snapshots: {
...request().snapshots,
normalizedInput: { issue: 'Different input' },
},
},
],
[
'lint result',
{
lint: {
exportReadiness: 'warning' as const,
findings: [
{
ruleId: 'PR001',
severity: 'warning' as const,
message: 'A warning',
source: 'prompt',
},
],
},
},
],
])('rejects a store response with different %s', async (_, changes) => {
const expected = request()
const mismatched: GeneratedRun = {
id: 'run-from-store',
...expected,
sourceDraftId: expected.sourceDraftId ?? null,
...(changes as Partial<GeneratedRun>),
generatedAt: '2026-07-27T12:00:00.000Z',
}
const store: GeneratedRunStore = {
createIdempotently: async () => ({ run: mismatched, created: false }),
}
await expect(
createGeneratedRun(dependencies(store), expected),
).rejects.toMatchObject({ code: 'generated_run_store_invariant_failed' })
})
})
@@ -0,0 +1,243 @@
import { createHash } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import { isDeepStrictEqual } from 'node:util'
export type ImmutableJsonValue =
| null
| boolean
| number
| string
| readonly ImmutableJsonValue[]
| ImmutableJsonObject
export interface ImmutableJsonObject {
readonly [key: string]: ImmutableJsonValue
}
export interface GeneratedRunLintFinding {
readonly ruleId: string
readonly severity: 'info' | 'warning' | 'error'
readonly message: string
readonly source: string
readonly controlPath?: string | null
}
export interface GeneratedRunLintResult {
readonly exportReadiness: 'ready' | 'warning' | 'blocked'
readonly findings: readonly GeneratedRunLintFinding[]
}
export interface GeneratedRunSnapshots {
readonly playbook: ImmutableJsonObject
readonly repositoryProfile: ImmutableJsonObject | null
readonly normalizedInput: ImmutableJsonObject
readonly policy: ImmutableJsonObject
readonly provenance: readonly ImmutableJsonValue[]
}
export interface CreateGeneratedRunRequest {
readonly workspaceId: string
readonly generatedBy: string
readonly sourceDraftId?: string | null
readonly playbookVersionId: string
readonly snapshots: GeneratedRunSnapshots
readonly lint: GeneratedRunLintResult
readonly renderedPrompt: string
readonly renderDigest: string
readonly idempotencyKey: string
}
export interface GeneratedRun {
readonly id: string
readonly workspaceId: string
readonly generatedBy: string
readonly sourceDraftId: string | null
readonly playbookVersionId: string
readonly snapshots: GeneratedRunSnapshots
readonly lint: GeneratedRunLintResult
readonly renderedPrompt: string
readonly renderDigest: string
readonly idempotencyKey: string
readonly generatedAt: string
}
export interface StoreGeneratedRunResult {
readonly run: GeneratedRun
readonly created: boolean
}
/**
* Implementations must atomically create by (workspaceId, idempotencyKey), or
* return the existing byte-for-byte logical run. Reusing a key for different
* input must fail with a generated_run_idempotency_conflict DomainError.
*/
export interface GeneratedRunStore {
createIdempotently(run: GeneratedRun): Promise<StoreGeneratedRunResult>
}
export interface GeneratedRunCreationDependencies {
readonly store: GeneratedRunStore
readonly nextId: () => string
readonly now: () => Date
}
function immutableJson(value: ImmutableJsonValue): ImmutableJsonValue {
if (Array.isArray(value)) {
return Object.freeze(value.map((item) => immutableJson(item)))
}
if (value !== null && typeof value === 'object') {
return Object.freeze(
Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, immutableJson(item)]),
),
)
}
return value
}
function immutableJsonObject(value: ImmutableJsonObject): ImmutableJsonObject {
return immutableJson(value) as ImmutableJsonObject
}
function immutableJsonArray(
value: readonly ImmutableJsonValue[],
): readonly ImmutableJsonValue[] {
return immutableJson(value) as readonly ImmutableJsonValue[]
}
function immutableLintResult(
lint: GeneratedRunLintResult,
): GeneratedRunLintResult {
return Object.freeze({
exportReadiness: lint.exportReadiness,
findings: Object.freeze(
lint.findings.map((finding) =>
Object.freeze({
ruleId: finding.ruleId,
severity: finding.severity,
message: finding.message,
source: finding.source,
...(finding.controlPath === undefined
? {}
: { controlPath: finding.controlPath }),
}),
),
),
})
}
function immutableSnapshots(
snapshots: GeneratedRunSnapshots,
): GeneratedRunSnapshots {
return Object.freeze({
playbook: immutableJsonObject(snapshots.playbook),
repositoryProfile:
snapshots.repositoryProfile === null
? null
: immutableJsonObject(snapshots.repositoryProfile),
normalizedInput: immutableJsonObject(snapshots.normalizedInput),
policy: immutableJsonObject(snapshots.policy),
provenance: immutableJsonArray(snapshots.provenance),
})
}
function immutableRun(run: GeneratedRun): GeneratedRun {
return Object.freeze({
...run,
snapshots: immutableSnapshots(run.snapshots),
lint: immutableLintResult(run.lint),
})
}
export function computeRenderDigest(renderedPrompt: string): string {
return createHash('sha256').update(renderedPrompt, 'utf8').digest('hex')
}
function assertFinalGenerationAllowed(lint: GeneratedRunLintResult): void {
const blockingFindings = lint.findings.filter(
(finding) => finding.severity === 'error',
)
if (lint.exportReadiness === 'blocked' || blockingFindings.length > 0) {
throw new DomainError(
'generated_run_lint_blocked',
'An immutable generated task cannot be created while prompt lint is blocked',
{
exportReadiness: lint.exportReadiness,
blockingRuleIds: blockingFindings.map((finding) => finding.ruleId),
},
)
}
}
function assertIdempotencyKey(idempotencyKey: string): void {
if (
idempotencyKey.length === 0 ||
idempotencyKey.length > 255 ||
idempotencyKey.trim() !== idempotencyKey
) {
throw new DomainError(
'generated_run_idempotency_key_invalid',
'Idempotency key must contain 1 to 255 characters without surrounding whitespace',
)
}
}
function assertStoredRunMatches(
request: CreateGeneratedRunRequest,
stored: GeneratedRun,
): void {
if (
stored.workspaceId !== request.workspaceId ||
stored.generatedBy !== request.generatedBy ||
stored.sourceDraftId !== (request.sourceDraftId ?? null) ||
stored.playbookVersionId !== request.playbookVersionId ||
stored.idempotencyKey !== request.idempotencyKey ||
stored.renderDigest !== request.renderDigest ||
stored.renderedPrompt !== request.renderedPrompt ||
!isDeepStrictEqual(stored.snapshots, request.snapshots) ||
!isDeepStrictEqual(stored.lint, request.lint)
) {
throw new DomainError(
'generated_run_store_invariant_failed',
'Generated-run store returned a record that does not match the creation request',
)
}
}
export async function createGeneratedRun(
dependencies: GeneratedRunCreationDependencies,
request: CreateGeneratedRunRequest,
): Promise<StoreGeneratedRunResult> {
assertIdempotencyKey(request.idempotencyKey)
assertFinalGenerationAllowed(request.lint)
const computedDigest = computeRenderDigest(request.renderedPrompt)
if (computedDigest !== request.renderDigest) {
throw new DomainError(
'generated_run_render_digest_mismatch',
'Rendered prompt does not match its declared SHA-256 digest',
{ declared: request.renderDigest, computed: computedDigest },
)
}
const candidate = immutableRun({
id: dependencies.nextId(),
workspaceId: request.workspaceId,
generatedBy: request.generatedBy,
sourceDraftId: request.sourceDraftId ?? null,
playbookVersionId: request.playbookVersionId,
snapshots: request.snapshots,
lint: request.lint,
renderedPrompt: request.renderedPrompt,
renderDigest: computedDigest,
idempotencyKey: request.idempotencyKey,
generatedAt: dependencies.now().toISOString(),
})
const result = await dependencies.store.createIdempotently(candidate)
assertStoredRunMatches(request, result.run)
return Object.freeze({
run: immutableRun(result.run),
created: result.created,
})
}
@@ -0,0 +1,37 @@
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import type { GeneratedRun } from './create-generated-run'
export interface GeneratedRunReader {
findByIdForWorkspace(
workspaceId: string,
runId: string,
): Promise<GeneratedRun | null>
}
export interface GetGeneratedRunDependencies {
readonly reader: GeneratedRunReader
readonly workspaceAuthorization: WorkspaceAuthorizationLookup
}
export async function getGeneratedRun(
dependencies: GetGeneratedRunDependencies,
request: {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly runId: string
},
): Promise<GeneratedRun | null> {
await authorizeWorkspaceAction(dependencies.workspaceAuthorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'read',
})
return dependencies.reader.findByIdForWorkspace(
request.workspaceId,
request.runId,
)
}
@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from 'vitest'
import type {
WorkspaceAuthorizationRecord,
WorkspaceRole,
} from '../auth/workspace-authorization'
import {
listGeneratedRuns,
type GeneratedRunHistoryReader,
} from './list-generated-runs'
const userId = '00000000-0000-4000-8000-000000000001'
const workspaceId = '00000000-0000-4000-8000-000000000002'
function authorization(role: WorkspaceRole): WorkspaceAuthorizationRecord {
return {
userId,
workspaceId,
workspaceRole: role,
instanceRole: 'user',
userStatus: 'active',
}
}
function dependencies(
role: WorkspaceRole = 'viewer',
record: WorkspaceAuthorizationRecord | null = authorization(role),
) {
const reader: GeneratedRunHistoryReader = {
listForWorkspace: vi.fn(async () => ({ items: [], nextCursor: null })),
}
return {
reader,
workspaceAuthorization: {
findWorkspaceAuthorization: vi.fn(async () => record),
},
}
}
describe('listGeneratedRuns', () => {
it('rejects unauthenticated requests before reading history', async () => {
const target = dependencies()
await expect(
listGeneratedRuns(target, { actor: null, workspaceId }),
).rejects.toMatchObject({ code: 'authentication_required' })
expect(target.reader.listForWorkspace).not.toHaveBeenCalled()
})
it('conceals missing memberships including instance administrators', async () => {
const target = dependencies('owner', null)
await expect(
listGeneratedRuns(target, { actor: { userId }, workspaceId }),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
expect(target.reader.listForWorkspace).not.toHaveBeenCalled()
})
it.each(['viewer', 'editor', 'owner'] as const)(
'allows %s to read bounded workspace history',
async (role) => {
const target = dependencies(role)
const query = {
cursor: 'opaque',
limit: 25,
playbookSlug: 'bounded-change',
repositoryId: '00000000-0000-4000-8000-000000000003',
}
await expect(
listGeneratedRuns(target, {
actor: { userId },
workspaceId,
query,
}),
).resolves.toEqual({ items: [], nextCursor: null })
expect(target.reader.listForWorkspace).toHaveBeenCalledWith(
workspaceId,
query,
)
},
)
})
@@ -0,0 +1,51 @@
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import type { GeneratedRun } from './create-generated-run'
export interface GeneratedRunHistoryQuery {
readonly cursor?: string | null
readonly limit?: number
readonly playbookSlug?: string
readonly repositoryId?: string
}
export interface GeneratedRunPage {
readonly items: readonly GeneratedRun[]
readonly nextCursor: string | null
}
export interface GeneratedRunHistoryReader {
listForWorkspace(
workspaceId: string,
query: GeneratedRunHistoryQuery,
): Promise<GeneratedRunPage>
}
export interface ListGeneratedRunsDependencies {
readonly reader: GeneratedRunHistoryReader
readonly workspaceAuthorization: WorkspaceAuthorizationLookup
}
export interface ListGeneratedRunsRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly query?: GeneratedRunHistoryQuery
}
export async function listGeneratedRuns(
dependencies: ListGeneratedRunsDependencies,
request: ListGeneratedRunsRequest,
): Promise<GeneratedRunPage> {
await authorizeWorkspaceAction(dependencies.workspaceAuthorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'read',
})
return dependencies.reader.listForWorkspace(
request.workspaceId,
request.query ?? {},
)
}
+40
View File
@@ -0,0 +1,40 @@
export interface Clock {
now(): Date
}
export interface IdGenerator {
next(): string
}
export * from './artifacts/generated-artifact'
export * from './artifacts/export-generated-run-artifact'
export * from './auth/session-policy'
export * from './auth/token-digest'
export * from './auth/auth-service'
export * from './auth/invitations'
export * from './auth/password-reset/password-reset'
export * from './auth/workspace-authorization'
export * from './composition/compose-and-create-generated-run'
export * from './composition/authoritative-composition'
export * from './composition/generate-composition-from-draft'
export * from './composition/composition-drafts'
export * from './generated-runs/create-generated-run'
export * from './generated-runs/get-generated-run'
export * from './generated-runs/list-generated-runs'
export * from './jobs/job-queue'
export * from './operations/operations'
export * from './operations/product-metrics'
export * from './integrations/gitea-connections'
export * from './integrations/gitea-repository-import'
export * from './library/playbook-favorites'
export * from './library/playbook-collections'
export * from './playbooks/import-built-in-playbooks'
export * from './playbooks/private-playbook-drafts'
export * from './playbooks/private-playbook-publication'
export * from './playbooks/private-playbook-quality'
export * from './quality/static-quality-evaluation'
export * from './quality/playbook-package-linter'
export * from './repositories/repository-profiles'
export * from './repositories/repository-preferences'
export * from './retention/artifact-retention'
export * from './setup/complete-first-run'
@@ -0,0 +1,388 @@
import { describe, expect, it } from 'vitest'
import type { WorkspaceAuthorizationLookup } from '../auth/workspace-authorization'
import {
createGiteaIntegration,
deleteGiteaIntegration,
discoverGiteaRepositories,
getGiteaIntegration,
listGiteaIntegrations,
rotateGiteaIntegrationSecret,
testGiteaIntegration,
type GiteaConnectionDependencies,
type GiteaIntegrationStore,
type GiteaIntegrationWithSecret,
type GiteaProbeResult,
type SafeGiteaIntegration,
type StoredSecretEnvelope,
} from './gitea-connections'
const workspaceId = '00000000-0000-4000-8000-000000000001'
const integrationId = '00000000-0000-4000-8000-000000000002'
function authorization(
role: 'viewer' | 'editor' | 'owner',
): WorkspaceAuthorizationLookup {
return {
async findWorkspaceAuthorization(userId, requestedWorkspaceId) {
if (requestedWorkspaceId !== workspaceId) return null
return {
userId,
workspaceId,
instanceRole: 'user',
workspaceRole: role,
userStatus: 'active',
}
},
}
}
function probe(
status: GiteaProbeResult['status'] = 'healthy',
): GiteaProbeResult {
return {
normalizedBaseUrl: 'https://git.example.test',
status,
serverVersion: '1.24.7',
remoteIdentity: { id: '7', login: 'devrunbook' },
capabilities: {
'repository-list': 'supported',
contents: 'supported',
},
healthCode: status === 'failed' ? 'AUTH_INVALID' : null,
warnings: status === 'degraded' ? ['Branch protection is forbidden.'] : [],
}
}
function envelope(token: string): StoredSecretEnvelope {
return {
envelopeVersion: 1,
keyVersion: 'v1',
nonce: new Uint8Array(12),
ciphertext: new TextEncoder().encode(token),
authTag: new Uint8Array(16),
lastFour: token.slice(-4),
}
}
function safe(
overrides: Partial<SafeGiteaIntegration> = {},
): SafeGiteaIntegration {
return {
id: integrationId,
workspaceId,
displayName: 'Primary Gitea',
baseUrl: 'https://git.example.test',
status: 'healthy',
capabilities: probe().capabilities,
serverVersion: '1.24.7',
remoteIdentity: { id: '7', login: 'devrunbook' },
healthCode: null,
lastCheckedAt: '2026-07-27T10:00:00.000Z',
secretLastFour: 'cret',
createdAt: '2026-07-27T10:00:00.000Z',
updatedAt: '2026-07-27T10:00:00.000Z',
...overrides,
}
}
class MemoryStore implements GiteaIntegrationStore {
current: GiteaIntegrationWithSecret | null = null
audit: string[] = []
async listSafeForWorkspace(requestedWorkspaceId: string) {
return this.current?.integration.workspaceId === requestedWorkspaceId
? [this.current.integration]
: []
}
async findSafeForWorkspace(
requestedWorkspaceId: string,
requestedId: string,
) {
return this.current?.integration.workspaceId === requestedWorkspaceId &&
this.current.integration.id === requestedId
? this.current.integration
: null
}
async findWithSecretForWorkspace(
requestedWorkspaceId: string,
requestedId: string,
) {
return this.current?.integration.workspaceId === requestedWorkspaceId &&
this.current.integration.id === requestedId
? this.current
: null
}
async createWithSecret(
request: Parameters<GiteaIntegrationStore['createWithSecret']>[0],
) {
const integration = safe({
id: request.id,
workspaceId: request.workspaceId,
displayName: request.displayName,
baseUrl: request.baseUrl,
status: request.probe.status === 'healthy' ? 'healthy' : 'degraded',
capabilities: request.probe.capabilities,
healthCode: request.probe.healthCode,
secretLastFour: request.secret.lastFour,
})
this.current = {
integration,
secret: request.secret,
allowPrivateHttp: request.allowPrivateHttp,
requestTimeoutMs: request.requestTimeoutMs,
}
this.audit.push('created')
return integration
}
async updateHealth(
request: Parameters<GiteaIntegrationStore['updateHealth']>[0],
) {
if (!this.current) return null
const integration = safe({
...this.current.integration,
status: request.probe.status === 'healthy' ? 'healthy' : 'degraded',
healthCode: request.probe.healthCode,
capabilities: request.probe.capabilities,
})
this.current = { ...this.current, integration }
this.audit.push('tested')
return integration
}
async rotateSecret(
request: Parameters<GiteaIntegrationStore['rotateSecret']>[0],
) {
if (!this.current) return null
const integration = safe({
...this.current.integration,
secretLastFour: request.secret.lastFour,
status: request.probe.status === 'healthy' ? 'healthy' : 'degraded',
})
this.current = { ...this.current, integration, secret: request.secret }
this.audit.push('rotated')
return integration
}
async deleteForWorkspace(
request: Parameters<GiteaIntegrationStore['deleteForWorkspace']>[0],
) {
if (
!this.current ||
this.current.integration.workspaceId !== request.workspaceId ||
this.current.integration.id !== request.integrationId
) {
return false
}
this.current = null
this.audit.push('deleted')
return true
}
}
function dependencies(role: 'viewer' | 'editor' | 'owner' = 'owner') {
const store = new MemoryStore()
const connectionCalls: Array<{ token: string; kind: string }> = []
const value: GiteaConnectionDependencies = {
authorization: authorization(role),
store,
ids: { next: () => integrationId },
cipher: {
encrypt: ({ plaintext }) => envelope(plaintext),
decrypt: ({ envelope: stored }) =>
new TextDecoder().decode(stored.ciphertext),
},
connection: {
async testConnection(request) {
connectionCalls.push({ kind: 'test', token: request.token })
return probe()
},
async listRepositories(request) {
connectionCalls.push({ kind: 'list', token: request.token })
return {
items: [
{
externalId: '42',
owner: 'team',
name: 'service',
defaultBranch: 'main',
archived: false,
private: true,
permissions: { pull: true, push: false, admin: false },
},
],
nextCursor: null,
}
},
},
}
return { value, store, connectionCalls }
}
const actor = { userId: '00000000-0000-4000-8000-000000000003' }
describe('Gitea connection use cases', () => {
it('tests before atomically storing an encrypted write-only token projection', async () => {
const { value, store, connectionCalls } = dependencies('editor')
const created = await createGiteaIntegration(value, {
actor,
workspaceId,
displayName: ' Primary Gitea ',
baseUrl: 'https://git.example.test/',
token: 'top-secret',
})
expect(created.displayName).toBe('Primary Gitea')
expect(created.baseUrl).toBe('https://git.example.test')
expect(created.secretLastFour).toBe('cret')
expect(created).not.toHaveProperty('token')
expect(created).not.toHaveProperty('secret')
expect(connectionCalls).toEqual([{ kind: 'test', token: 'top-secret' }])
expect(store.audit).toEqual(['created'])
})
it('does not persist a connection whose credentials cannot be verified', async () => {
const { value, store } = dependencies('editor')
value.connection.testConnection = async () => probe('failed')
await expect(
createGiteaIntegration(value, {
actor,
workspaceId,
displayName: 'Rejected Gitea',
baseUrl: 'https://git.example.test',
token: 'invalid-token',
}),
).rejects.toMatchObject({ code: 'gitea_connection_failed' })
expect(store.current).toBeNull()
expect(store.audit).toEqual([])
})
it('allows viewers to read safe metadata but not create or test connections', async () => {
const { value, store } = dependencies('viewer')
store.current = {
integration: safe(),
secret: envelope('top-secret'),
allowPrivateHttp: false,
requestTimeoutMs: 15_000,
}
await expect(
listGiteaIntegrations(value, { actor, workspaceId }),
).resolves.toHaveLength(1)
await expect(
getGiteaIntegration(value, { actor, workspaceId, integrationId }),
).resolves.toMatchObject({ id: integrationId })
await expect(
testGiteaIntegration(value, { actor, workspaceId, integrationId }),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
createGiteaIntegration(value, {
actor,
workspaceId,
displayName: 'Denied',
baseUrl: 'https://git.example.test',
token: 'secret',
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
})
it('decrypts only inside test/discovery and rotates after candidate validation', async () => {
const { value, store, connectionCalls } = dependencies('owner')
store.current = {
integration: safe(),
secret: envelope('old-secret'),
allowPrivateHttp: false,
requestTimeoutMs: 15_000,
}
await expect(
testGiteaIntegration(value, { actor, workspaceId, integrationId }),
).resolves.toMatchObject({ status: 'healthy' })
await expect(
discoverGiteaRepositories(value, {
actor,
workspaceId,
integrationId,
}),
).resolves.toMatchObject({ items: [{ externalId: '42' }] })
const rotated = await rotateGiteaIntegrationSecret(value, {
actor,
workspaceId,
integrationId,
token: 'new-secret',
})
expect(rotated.secretLastFour).toBe('cret')
expect(connectionCalls).toEqual([
{ kind: 'test', token: 'old-secret' },
{ kind: 'list', token: 'old-secret' },
{ kind: 'test', token: 'new-secret' },
])
expect(store.audit).toEqual(['tested', 'rotated'])
})
it('conceals cross-workspace records and reserves deletion for owners', async () => {
const editor = dependencies('editor')
editor.store.current = {
integration: safe(),
secret: envelope('secret'),
allowPrivateHttp: false,
requestTimeoutMs: 15_000,
}
await expect(
getGiteaIntegration(editor.value, {
actor,
workspaceId: '00000000-0000-4000-8000-000000000099',
integrationId,
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
deleteGiteaIntegration(editor.value, {
actor,
workspaceId,
integrationId,
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
const owner = dependencies('owner')
owner.store.current = editor.store.current
await expect(
deleteGiteaIntegration(owner.value, {
actor,
workspaceId,
integrationId,
}),
).resolves.toBeUndefined()
expect(owner.store.current).toBeNull()
})
it('rejects oversized tokens, invalid timeouts and page limits before I/O', async () => {
const { value, store } = dependencies('owner')
await expect(
createGiteaIntegration(value, {
actor,
workspaceId,
displayName: 'Gitea',
baseUrl: 'https://git.example.test',
token: ' secret ',
}),
).rejects.toMatchObject({ code: 'gitea_integration_invalid' })
store.current = {
integration: safe(),
secret: envelope('secret'),
allowPrivateHttp: false,
requestTimeoutMs: 15_000,
}
await expect(
discoverGiteaRepositories(value, {
actor,
workspaceId,
integrationId,
limit: 101,
}),
).rejects.toMatchObject({ code: 'gitea_integration_invalid' })
})
})
@@ -0,0 +1,459 @@
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
export const forgeCapabilityNames = [
'repository-list',
'repository-metadata',
'branches',
'tags',
'releases',
'contents',
'branch-protection',
'templates',
'workflows',
'topics',
'languages',
'permissions',
] as const
export type ForgeCapabilityName = (typeof forgeCapabilityNames)[number]
export type ForgeCapabilityState =
'supported' | 'unsupported' | 'forbidden' | 'temporarily_unavailable'
export type GiteaSafeErrorCode =
| 'AUTH_INVALID'
| 'PERMISSION_MISSING'
| 'CAPABILITY_UNSUPPORTED'
| 'RATE_LIMITED'
| 'NETWORK_BLOCKED'
| 'TLS_ERROR'
| 'REMOTE_UNAVAILABLE'
| 'CONTENT_TOO_LARGE'
export interface GiteaRemoteIdentity {
readonly id: string
readonly login: string
}
export interface SafeGiteaIntegration {
readonly id: string
readonly workspaceId: string
readonly displayName: string
readonly baseUrl: string
readonly status: 'configured' | 'healthy' | 'degraded' | 'disabled'
readonly capabilities: Readonly<
Partial<Record<ForgeCapabilityName, ForgeCapabilityState>>
>
readonly serverVersion: string | null
readonly remoteIdentity: GiteaRemoteIdentity | null
readonly healthCode: GiteaSafeErrorCode | null
readonly lastCheckedAt: string | null
readonly secretLastFour: string | null
readonly createdAt: string
readonly updatedAt: string
}
export interface GiteaProbeResult {
readonly normalizedBaseUrl: string
readonly status: 'healthy' | 'degraded' | 'failed'
readonly serverVersion: string | null
readonly remoteIdentity: GiteaRemoteIdentity | null
readonly capabilities: Readonly<
Partial<Record<ForgeCapabilityName, ForgeCapabilityState>>
>
readonly healthCode: GiteaSafeErrorCode | null
readonly warnings: readonly string[]
}
export interface ExternalGiteaRepository {
readonly externalId: string
readonly owner: string
readonly name: string
readonly defaultBranch: string | null
readonly archived: boolean
readonly private: boolean
readonly permissions: Readonly<{
pull: boolean
push: boolean
admin: boolean
}>
}
export interface ExternalGiteaRepositoryPage {
readonly items: readonly ExternalGiteaRepository[]
readonly nextCursor: string | null
}
export interface StoredSecretEnvelope {
readonly envelopeVersion: number
readonly keyVersion: string
readonly nonce: Uint8Array
readonly ciphertext: Uint8Array
readonly authTag: Uint8Array
readonly lastFour: string
}
export interface IntegrationSecretCipher {
encrypt(request: {
readonly workspaceId: string
readonly integrationId: string
readonly secretKind: 'access-token'
readonly plaintext: string
}): StoredSecretEnvelope
decrypt(request: {
readonly workspaceId: string
readonly integrationId: string
readonly secretKind: 'access-token'
readonly envelope: StoredSecretEnvelope
}): string
}
export interface GiteaConnectionPort {
testConnection(request: {
readonly baseUrl: string
readonly token: string
readonly allowPrivateHttp: boolean
readonly requestTimeoutMs: number
}): Promise<GiteaProbeResult>
listRepositories(request: {
readonly baseUrl: string
readonly token: string
readonly allowPrivateHttp: boolean
readonly requestTimeoutMs: number
readonly cursor: string | null
readonly limit: number
}): Promise<ExternalGiteaRepositoryPage>
}
export interface CreateStoredGiteaIntegrationRequest {
readonly id: string
readonly workspaceId: string
readonly createdBy: string
readonly displayName: string
readonly baseUrl: string
readonly allowPrivateHttp: boolean
readonly requestTimeoutMs: number
readonly secret: StoredSecretEnvelope
readonly probe: GiteaProbeResult
}
export interface GiteaIntegrationWithSecret {
readonly integration: SafeGiteaIntegration
readonly secret: StoredSecretEnvelope
readonly allowPrivateHttp: boolean
readonly requestTimeoutMs: number
}
export interface GiteaIntegrationStore {
listSafeForWorkspace(
workspaceId: string,
): Promise<readonly SafeGiteaIntegration[]>
findSafeForWorkspace(
workspaceId: string,
integrationId: string,
): Promise<SafeGiteaIntegration | null>
findWithSecretForWorkspace(
workspaceId: string,
integrationId: string,
): Promise<GiteaIntegrationWithSecret | null>
createWithSecret(
request: CreateStoredGiteaIntegrationRequest,
): Promise<SafeGiteaIntegration>
updateHealth(request: {
readonly workspaceId: string
readonly integrationId: string
readonly actorId: string
readonly probe: GiteaProbeResult
}): Promise<SafeGiteaIntegration | null>
rotateSecret(request: {
readonly workspaceId: string
readonly integrationId: string
readonly actorId: string
readonly secret: StoredSecretEnvelope
readonly probe: GiteaProbeResult
}): Promise<SafeGiteaIntegration | null>
deleteForWorkspace(request: {
readonly workspaceId: string
readonly integrationId: string
readonly actorId: string
}): Promise<boolean>
}
export interface GiteaConnectionDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly store: GiteaIntegrationStore
readonly connection: GiteaConnectionPort
readonly cipher: IntegrationSecretCipher
readonly ids: { next(): string }
}
export interface GiteaActorRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
}
function invalidInput(path: string, message: string): never {
throw new DomainError(
'gitea_integration_invalid',
'Gitea connection is invalid',
{
issues: [{ path, code: 'invalid', message, remediation: message }],
},
)
}
function integrationNotFound(): never {
throw new DomainError(
'gitea_integration_not_found',
'Gitea integration not found',
)
}
function displayName(value: string): string {
const normalized = value.trim()
if (normalized.length === 0 || normalized.length > 120) {
invalidInput(
'/displayName',
'Use a display name containing 1 to 120 characters.',
)
}
return normalized
}
function accessToken(value: string): string {
if (value.length < 4 || value.length > 4096 || value.trim() !== value) {
invalidInput(
'/token',
'Use a token containing 4 to 4096 characters without surrounding whitespace.',
)
}
return value
}
function timeout(value: number | undefined): number {
const resolved = value ?? 15_000
if (!Number.isInteger(resolved) || resolved < 1_000 || resolved > 60_000) {
invalidInput(
'/requestTimeoutMs',
'Use a request timeout from 1000 through 60000 milliseconds.',
)
}
return resolved
}
function pageLimit(value: number | undefined): number {
const resolved = value ?? 50
if (!Number.isInteger(resolved) || resolved < 1 || resolved > 100) {
invalidInput('/limit', 'Use a repository page limit from 1 through 100.')
}
return resolved
}
async function authorize(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest,
action: 'read' | 'write' | 'destructive',
): Promise<string> {
const actor = await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action,
})
return actor.userId
}
function persistedStatus(result: GiteaProbeResult): 'healthy' | 'degraded' {
return result.status === 'healthy' ? 'healthy' : 'degraded'
}
export async function listGiteaIntegrations(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest,
): Promise<readonly SafeGiteaIntegration[]> {
await authorize(dependencies, request, 'read')
return dependencies.store.listSafeForWorkspace(request.workspaceId)
}
export async function getGiteaIntegration(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & { readonly integrationId: string },
): Promise<SafeGiteaIntegration> {
await authorize(dependencies, request, 'read')
return (
(await dependencies.store.findSafeForWorkspace(
request.workspaceId,
request.integrationId,
)) ?? integrationNotFound()
)
}
export async function createGiteaIntegration(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & {
readonly displayName: string
readonly baseUrl: string
readonly token: string
readonly allowPrivateHttp?: boolean
readonly requestTimeoutMs?: number
},
): Promise<SafeGiteaIntegration> {
const createdBy = await authorize(dependencies, request, 'write')
const name = displayName(request.displayName)
const token = accessToken(request.token)
const requestTimeoutMs = timeout(request.requestTimeoutMs)
const allowPrivateHttp = request.allowPrivateHttp ?? false
const probe = await dependencies.connection.testConnection({
baseUrl: request.baseUrl,
token,
allowPrivateHttp,
requestTimeoutMs,
})
if (probe.status === 'failed') {
throw new DomainError(
'gitea_connection_failed',
'The Gitea connection could not be verified',
{ code: probe.healthCode },
)
}
const id = dependencies.ids.next()
const secret = dependencies.cipher.encrypt({
workspaceId: request.workspaceId,
integrationId: id,
secretKind: 'access-token',
plaintext: token,
})
return dependencies.store.createWithSecret({
id,
workspaceId: request.workspaceId,
createdBy,
displayName: name,
baseUrl: probe.normalizedBaseUrl,
allowPrivateHttp,
requestTimeoutMs,
secret,
probe: { ...probe, status: persistedStatus(probe) },
})
}
async function loadSecret(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & { readonly integrationId: string },
): Promise<GiteaIntegrationWithSecret & { readonly token: string }> {
const stored = await dependencies.store.findWithSecretForWorkspace(
request.workspaceId,
request.integrationId,
)
if (!stored) integrationNotFound()
return {
...stored,
token: dependencies.cipher.decrypt({
workspaceId: request.workspaceId,
integrationId: request.integrationId,
secretKind: 'access-token',
envelope: stored.secret,
}),
}
}
export async function testGiteaIntegration(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & { readonly integrationId: string },
): Promise<GiteaProbeResult> {
const actorId = await authorize(dependencies, request, 'write')
const stored = await loadSecret(dependencies, request)
const probe = await dependencies.connection.testConnection({
baseUrl: stored.integration.baseUrl,
token: stored.token,
allowPrivateHttp: stored.allowPrivateHttp,
requestTimeoutMs: stored.requestTimeoutMs,
})
const updated = await dependencies.store.updateHealth({
workspaceId: request.workspaceId,
integrationId: request.integrationId,
actorId,
probe,
})
if (!updated) integrationNotFound()
return probe
}
export async function discoverGiteaRepositories(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & {
readonly integrationId: string
readonly cursor?: string | null
readonly limit?: number
},
): Promise<ExternalGiteaRepositoryPage> {
await authorize(dependencies, request, 'read')
const stored = await loadSecret(dependencies, request)
if (stored.integration.status === 'disabled') {
throw new DomainError(
'gitea_integration_disabled',
'The Gitea integration is disabled',
)
}
return dependencies.connection.listRepositories({
baseUrl: stored.integration.baseUrl,
token: stored.token,
allowPrivateHttp: stored.allowPrivateHttp,
requestTimeoutMs: stored.requestTimeoutMs,
cursor: request.cursor ?? null,
limit: pageLimit(request.limit),
})
}
export async function rotateGiteaIntegrationSecret(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & {
readonly integrationId: string
readonly token: string
},
): Promise<SafeGiteaIntegration> {
const actorId = await authorize(dependencies, request, 'write')
const stored = await dependencies.store.findWithSecretForWorkspace(
request.workspaceId,
request.integrationId,
)
if (!stored) integrationNotFound()
const token = accessToken(request.token)
const probe = await dependencies.connection.testConnection({
baseUrl: stored.integration.baseUrl,
token,
allowPrivateHttp: stored.allowPrivateHttp,
requestTimeoutMs: stored.requestTimeoutMs,
})
const secret = dependencies.cipher.encrypt({
workspaceId: request.workspaceId,
integrationId: request.integrationId,
secretKind: 'access-token',
plaintext: token,
})
return (
(await dependencies.store.rotateSecret({
workspaceId: request.workspaceId,
integrationId: request.integrationId,
actorId,
secret,
probe,
})) ?? integrationNotFound()
)
}
export async function deleteGiteaIntegration(
dependencies: GiteaConnectionDependencies,
request: GiteaActorRequest & { readonly integrationId: string },
): Promise<void> {
const actorId = await authorize(dependencies, request, 'destructive')
const deleted = await dependencies.store.deleteForWorkspace({
workspaceId: request.workspaceId,
integrationId: request.integrationId,
actorId,
})
if (!deleted) integrationNotFound()
}
@@ -0,0 +1,291 @@
import { describe, expect, it, vi } from 'vitest'
import type { WorkspaceAuthorizationLookup } from '../auth/workspace-authorization'
import type { JobRecord, JobStore } from '../jobs/job-queue'
import type { SafeGiteaIntegration } from './gitea-connections'
import {
GITEA_REPOSITORY_SNAPSHOT_JOB,
importGiteaRepository,
refreshGiteaRepositorySnapshot,
type CollectingRepositorySnapshot,
type GiteaRepositoryImportStore,
type GiteaRepositorySnapshotDependencies,
type ImportedGiteaRepository,
} from './gitea-repository-import'
const workspaceId = '00000000-0000-4000-8000-000000000001'
const integrationId = '00000000-0000-4000-8000-000000000002'
const repositoryId = '00000000-0000-4000-8000-000000000003'
const jobId = '00000000-0000-4000-8000-000000000004'
const snapshotId = '00000000-0000-4000-8000-000000000005'
const now = new Date('2026-07-27T12:00:00.000Z')
function authorization(
role: 'viewer' | 'editor' | 'owner',
): WorkspaceAuthorizationLookup {
return {
async findWorkspaceAuthorization(userId, requestedWorkspaceId) {
return requestedWorkspaceId === workspaceId
? {
userId,
workspaceId,
instanceRole: 'user',
workspaceRole: role,
userStatus: 'active',
}
: null
},
}
}
function integration(
status: SafeGiteaIntegration['status'] = 'healthy',
): SafeGiteaIntegration {
return {
id: integrationId,
workspaceId,
displayName: 'Primary Gitea',
baseUrl: 'https://git.example.test',
status,
capabilities: { 'repository-list': 'supported' },
serverVersion: '1.24.7',
remoteIdentity: { id: '7', login: 'devrunbook' },
healthCode: null,
lastCheckedAt: now.toISOString(),
secretLastFour: 'cret',
createdAt: now.toISOString(),
updatedAt: now.toISOString(),
}
}
function imported(created = true): ImportedGiteaRepository {
return {
id: repositoryId,
workspaceId,
integrationId,
externalId: '42',
owner: 'acme',
name: 'console',
displayName: 'acme/console',
defaultBranch: 'main',
archived: false,
created,
}
}
function job(): JobRecord {
return {
id: jobId,
workspaceId,
type: GITEA_REPOSITORY_SNAPSHOT_JOB,
state: 'queued',
idempotencyKey: 'digest',
payload: {},
progress: {},
attemptCount: 0,
maxAttempts: 3,
leaseOwner: null,
leaseExpiresAt: null,
availableAt: now,
startedAt: null,
finishedAt: null,
errorCode: null,
errorDetailRedacted: null,
createdAt: now,
updatedAt: now,
}
}
function snapshot(): CollectingRepositorySnapshot {
return {
id: snapshotId,
repositoryId,
integrationId,
state: 'collecting',
capturedAt: null,
capabilities: {},
evidence: {},
evidenceDigest: null,
syncJobId: jobId,
createdAt: now.toISOString(),
}
}
function dependencies(
role: 'viewer' | 'editor' | 'owner' = 'editor',
): GiteaRepositorySnapshotDependencies & {
store: GiteaRepositoryImportStore & {
importExternalRepository: ReturnType<typeof vi.fn>
}
jobs: JobStore & { enqueue: ReturnType<typeof vi.fn> }
snapshots: {
beginCollection: ReturnType<typeof vi.fn>
}
} {
const repository = imported()
return {
authorization: authorization(role),
store: {
findSafeForWorkspace: vi.fn(async () => integration()),
importExternalRepository: vi.fn(async () => repository),
findImportedRepositoryForWorkspace: vi.fn(async () => ({
id: repository.id,
workspaceId: repository.workspaceId,
integrationId: repository.integrationId,
externalId: repository.externalId,
owner: repository.owner,
name: repository.name,
displayName: repository.displayName,
defaultBranch: repository.defaultBranch,
archived: repository.archived,
})),
},
jobs: {
enqueue: vi.fn(async (request) => ({
job: { ...job(), idempotencyKey: request.idempotencyKey },
created: true,
})),
claim: vi.fn(),
heartbeat: vi.fn(),
succeed: vi.fn(),
retry: vi.fn(),
fail: vi.fn(),
findForWorkspace: vi.fn(),
},
snapshots: { beginCollection: vi.fn(async () => snapshot()) },
now: () => now,
}
}
const selectedRepository = {
externalId: '42',
owner: 'acme',
name: 'console',
defaultBranch: 'main',
archived: false,
private: true,
permissions: { pull: true, push: false, admin: false },
} as const
describe('Gitea repository import and snapshot orchestration', () => {
it('imports identity and queues only a governed read-only snapshot payload', async () => {
const adapter = dependencies()
const result = await importGiteaRepository(adapter, {
actor: { userId: 'operator' },
workspaceId,
integrationId,
repository: selectedRepository,
})
expect(result).toMatchObject({
repositoryCreated: true,
jobCreated: true,
snapshot: { state: 'collecting', syncJobId: jobId },
})
expect(adapter.jobs.enqueue).toHaveBeenCalledWith(
expect.objectContaining({
type: GITEA_REPOSITORY_SNAPSHOT_JOB,
availableAt: new Date(now.getTime() + 5_000),
payload: {
schemaVersion: 1,
workspaceId,
integrationId,
repositoryId,
requestedBy: 'operator',
collectionMode: 'bounded-read-only',
profileRevisionPolicy: 'create-initial-only',
},
}),
)
expect(JSON.stringify(adapter.jobs.enqueue.mock.calls)).not.toContain(
'command',
)
})
it('uses stable import idempotency and binds the collecting snapshot to the job', async () => {
const adapter = dependencies()
await importGiteaRepository(adapter, {
actor: { userId: 'operator' },
workspaceId,
integrationId,
repository: selectedRepository,
})
await importGiteaRepository(adapter, {
actor: { userId: 'operator' },
workspaceId,
integrationId,
repository: selectedRepository,
})
expect(adapter.jobs.enqueue.mock.calls[0]![0].idempotencyKey).toBe(
adapter.jobs.enqueue.mock.calls[1]![0].idempotencyKey,
)
expect(adapter.snapshots.beginCollection).toHaveBeenCalledWith({
workspaceId,
repositoryId,
integrationId,
syncJobId: jobId,
now,
})
})
it('requires editor access before touching integration or job state', async () => {
const adapter = dependencies('viewer')
await expect(
importGiteaRepository(adapter, {
actor: { userId: 'viewer' },
workspaceId,
integrationId,
repository: selectedRepository,
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
expect(adapter.store.importExternalRepository).not.toHaveBeenCalled()
expect(adapter.jobs.enqueue).not.toHaveBeenCalled()
})
it('queues explicit refresh without mutating a profile revision', async () => {
const adapter = dependencies()
await refreshGiteaRepositorySnapshot(adapter, {
actor: { userId: 'operator' },
workspaceId,
repositoryId,
idempotencyKey: 'refresh-button-1',
})
expect(adapter.jobs.enqueue).toHaveBeenCalledWith(
expect.objectContaining({
payload: expect.objectContaining({
repositoryId,
profileRevisionPolicy: 'create-initial-only',
}),
}),
)
expect(adapter.store.importExternalRepository).not.toHaveBeenCalled()
})
it('rejects disabled integrations and invalid refresh keys safely', async () => {
const adapter = dependencies()
adapter.store.findSafeForWorkspace = vi.fn(async () =>
integration('disabled'),
)
await expect(
importGiteaRepository(adapter, {
actor: { userId: 'operator' },
workspaceId,
integrationId,
repository: selectedRepository,
}),
).rejects.toMatchObject({ code: 'gitea_integration_disabled' })
const enabled = dependencies()
await expect(
refreshGiteaRepositorySnapshot(enabled, {
actor: { userId: 'operator' },
workspaceId,
repositoryId,
idempotencyKey: ' ',
}),
).rejects.toMatchObject({ code: 'gitea_repository_import_invalid' })
expect(enabled.jobs.enqueue).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,323 @@
import { createHash } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import {
enqueueJob,
type JobJsonValue,
type JobRecord,
type JobStore,
} from '../jobs/job-queue'
import type {
ExternalGiteaRepository,
GiteaIntegrationStore,
} from './gitea-connections'
export const GITEA_REPOSITORY_SNAPSHOT_JOB =
'gitea.repository-snapshot' as const
export interface ImportedGiteaRepository {
readonly id: string
readonly workspaceId: string
readonly integrationId: string
readonly externalId: string
readonly owner: string
readonly name: string
readonly displayName: string
readonly defaultBranch: string | null
readonly archived: boolean
readonly created: boolean
}
export type ImportedGiteaRepositoryIdentity = Omit<
ImportedGiteaRepository,
'created'
>
export interface GiteaRepositoryImportStore extends Pick<
GiteaIntegrationStore,
'findSafeForWorkspace'
> {
/**
* Upserts only normalized repository identity fields. It must never inspect
* repository content or execute commands found in remote data.
*/
importExternalRepository(request: {
readonly workspaceId: string
readonly integrationId: string
readonly repository: ExternalGiteaRepository
readonly now?: Date
}): Promise<ImportedGiteaRepository | null>
findImportedRepositoryForWorkspace(
workspaceId: string,
repositoryId: string,
): Promise<ImportedGiteaRepositoryIdentity | null>
}
export interface CollectingRepositorySnapshot {
readonly id: string
readonly repositoryId: string
readonly integrationId: string | null
readonly state: 'collecting' | 'complete' | 'failed' | 'cancelled'
readonly capturedAt: string | null
readonly capabilities: JobJsonValue
readonly evidence: JobJsonValue
readonly evidenceDigest: string | null
readonly syncJobId: string | null
readonly createdAt: string
}
export interface RepositorySnapshotCollectionStore {
/**
* Creates a collecting snapshot or returns the snapshot already bound to the
* same sync job. Repository/integration ownership must be checked atomically.
*/
beginCollection(request: {
readonly workspaceId: string
readonly repositoryId: string
readonly integrationId: string
readonly syncJobId: string | null
readonly now?: Date
}): Promise<CollectingRepositorySnapshot | null>
}
export interface GiteaRepositorySnapshotDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly store: GiteaRepositoryImportStore
readonly snapshots: RepositorySnapshotCollectionStore
readonly jobs: JobStore
readonly now: () => Date
}
export interface GiteaRepositoryActorRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
}
export interface ImportGiteaRepositoryRequest extends GiteaRepositoryActorRequest {
readonly integrationId: string
readonly repository: ExternalGiteaRepository
}
export interface RefreshGiteaRepositorySnapshotRequest extends GiteaRepositoryActorRequest {
readonly repositoryId: string
/** A caller-owned retry key. A new deliberate refresh uses a new key. */
readonly idempotencyKey: string
}
export interface QueuedRepositorySnapshot {
readonly repository: ImportedGiteaRepositoryIdentity
readonly snapshot: CollectingRepositorySnapshot
readonly job: JobRecord
readonly repositoryCreated: boolean
readonly jobCreated: boolean
}
function invalidInput(path: string, message: string): never {
throw new DomainError(
'gitea_repository_import_invalid',
'Gitea repository import is invalid',
{ issues: [{ path, code: 'invalid', message, remediation: message }] },
)
}
function boundedText(value: string, path: string, maximum: number): string {
const normalized = value.trim()
if (
normalized.length === 0 ||
normalized.length > maximum ||
[...normalized].some((character) => character.charCodeAt(0) < 0x20)
) {
invalidInput(path, `Use 1 to ${maximum} printable characters.`)
}
return normalized
}
function normalizedRepository(
repository: ExternalGiteaRepository,
): ExternalGiteaRepository {
return Object.freeze({
externalId: boundedText(repository.externalId, '/externalId', 255),
owner: boundedText(repository.owner, '/owner', 255),
name: boundedText(repository.name, '/name', 255),
defaultBranch:
repository.defaultBranch === null
? null
: boundedText(repository.defaultBranch, '/defaultBranch', 255),
archived: repository.archived,
private: repository.private,
permissions: Object.freeze({
pull: repository.permissions.pull,
push: repository.permissions.push,
admin: repository.permissions.admin,
}),
})
}
function digestIdempotency(parts: readonly string[]): string {
return createHash('sha256')
.update(parts.map((part) => `${part.length}:${part}`).join('|'))
.digest('hex')
}
async function authorizeEditor(
dependencies: GiteaRepositorySnapshotDependencies,
request: GiteaRepositoryActorRequest,
): Promise<void> {
await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'write',
})
}
async function requireEnabledIntegration(
dependencies: GiteaRepositorySnapshotDependencies,
workspaceId: string,
integrationId: string,
): Promise<void> {
const integration = await dependencies.store.findSafeForWorkspace(
workspaceId,
integrationId,
)
if (!integration) {
throw new DomainError(
'gitea_integration_not_found',
'Gitea integration not found',
)
}
if (integration.status === 'disabled') {
throw new DomainError(
'gitea_integration_disabled',
'The Gitea integration is disabled',
)
}
}
async function queueSnapshot(
dependencies: GiteaRepositorySnapshotDependencies,
request: {
readonly requestedBy: string
readonly workspaceId: string
readonly integrationId: string
readonly repository: ImportedGiteaRepositoryIdentity
readonly idempotencyKey: string
readonly repositoryCreated: boolean
},
): Promise<QueuedRepositorySnapshot> {
const queuedAt = dependencies.now()
const queued = await enqueueJob(dependencies.jobs, {
workspaceId: request.workspaceId,
type: GITEA_REPOSITORY_SNAPSHOT_JOB,
idempotencyKey: request.idempotencyKey,
payload: {
schemaVersion: 1,
workspaceId: request.workspaceId,
integrationId: request.integrationId,
repositoryId: request.repository.id,
requestedBy: request.requestedBy,
collectionMode: 'bounded-read-only',
profileRevisionPolicy: 'create-initial-only',
},
maxAttempts: 3,
availableAt: new Date(queuedAt.getTime() + 5_000),
})
const snapshot = await dependencies.snapshots.beginCollection({
workspaceId: request.workspaceId,
repositoryId: request.repository.id,
integrationId: request.integrationId,
syncJobId: queued.job.id,
now: queuedAt,
})
if (!snapshot) {
throw new DomainError(
'repository_snapshot_target_not_found',
'Repository snapshot target was not found',
)
}
return {
repository: request.repository,
snapshot,
job: queued.job,
repositoryCreated: request.repositoryCreated,
jobCreated: queued.created,
}
}
export async function importGiteaRepository(
dependencies: GiteaRepositorySnapshotDependencies,
request: ImportGiteaRepositoryRequest,
): Promise<QueuedRepositorySnapshot> {
await authorizeEditor(dependencies, request)
await requireEnabledIntegration(
dependencies,
request.workspaceId,
request.integrationId,
)
const repository = normalizedRepository(request.repository)
const imported = await dependencies.store.importExternalRepository({
workspaceId: request.workspaceId,
integrationId: request.integrationId,
repository,
now: dependencies.now(),
})
if (!imported) {
throw new DomainError(
'gitea_integration_not_found',
'Gitea integration not found',
)
}
const { created, ...identity } = imported
return queueSnapshot(dependencies, {
requestedBy: request.actor!.userId,
workspaceId: request.workspaceId,
integrationId: request.integrationId,
repository: identity,
repositoryCreated: created,
idempotencyKey: digestIdempotency([
'gitea-import-v1',
request.workspaceId,
request.integrationId,
repository.externalId,
]),
})
}
export async function refreshGiteaRepositorySnapshot(
dependencies: GiteaRepositorySnapshotDependencies,
request: RefreshGiteaRepositorySnapshotRequest,
): Promise<QueuedRepositorySnapshot> {
await authorizeEditor(dependencies, request)
const refreshKey = boundedText(request.idempotencyKey, '/idempotencyKey', 128)
const repository =
await dependencies.store.findImportedRepositoryForWorkspace(
request.workspaceId,
request.repositoryId,
)
if (!repository) {
throw new DomainError('repository_not_found', 'Repository not found')
}
await requireEnabledIntegration(
dependencies,
request.workspaceId,
repository.integrationId,
)
return queueSnapshot(dependencies, {
requestedBy: request.actor!.userId,
workspaceId: request.workspaceId,
integrationId: repository.integrationId,
repository,
repositoryCreated: false,
idempotencyKey: digestIdempotency([
'gitea-refresh-v1',
request.workspaceId,
repository.id,
refreshKey,
]),
})
}
@@ -0,0 +1,167 @@
import { describe, expect, it, vi } from 'vitest'
import {
enqueueJob,
PermanentJobError,
processNextJob,
TransientJobError,
type JobRecord,
type JobStore,
} from './job-queue'
function job(overrides: Partial<JobRecord> = {}): JobRecord {
const timestamp = new Date('2026-07-27T12:00:00.000Z')
return {
id: '00000000-0000-4000-8000-000000000901',
workspaceId: '00000000-0000-4000-8000-000000000902',
type: 'safe-test',
state: 'running',
idempotencyKey: 'test-1',
payload: {},
progress: {},
attemptCount: 1,
maxAttempts: 3,
leaseOwner: 'worker:lease',
leaseExpiresAt: new Date(timestamp.getTime() + 60_000),
availableAt: timestamp,
startedAt: timestamp,
finishedAt: null,
errorCode: null,
errorDetailRedacted: null,
createdAt: timestamp,
updatedAt: timestamp,
...overrides,
}
}
function store(claimed: JobRecord | null = job()) {
return {
enqueue: vi.fn(async (request) => ({
job: job({
workspaceId: request.workspaceId,
type: request.type,
idempotencyKey: request.idempotencyKey,
payload: request.payload,
}),
created: true,
})),
claim: vi.fn(async () => claimed),
heartbeat: vi.fn(async () => true),
succeed: vi.fn(async () => true),
retry: vi.fn(async () => true),
fail: vi.fn(async () => true),
findForWorkspace: vi.fn(async () => null),
} satisfies JobStore
}
describe('job application service', () => {
it('requires explicit bounded idempotency and retry input', async () => {
const adapter = store(null)
await expect(
enqueueJob(adapter, {
workspaceId: null,
type: 'system.health-probe',
idempotencyKey: '',
payload: {},
}),
).rejects.toMatchObject({ code: 'job_idempotency_key_invalid' })
expect(adapter.enqueue).not.toHaveBeenCalled()
})
it('completes a registered safe handler under its unique lease', async () => {
const adapter = store()
const result = await processNextJob({
store: adapter,
handlers: { 'safe-test': async () => ({ phase: 'complete' }) },
workerId: 'worker-a',
nextLeaseId: () => 'lease-a',
leaseDurationMs: 30_000,
})
expect(result).toMatchObject({ outcome: 'succeeded' })
expect(adapter.claim).toHaveBeenCalledWith({
leaseOwner: 'worker-a:lease-a',
leaseDurationMs: 30_000,
})
expect(adapter.succeed).toHaveBeenCalledWith(job().id, 'worker-a:lease-a', {
phase: 'complete',
})
})
it('retries only classified transient failures with bounded backoff', async () => {
const adapter = store()
const result = await processNextJob({
store: adapter,
handlers: {
'safe-test': async () => {
throw new TransientJobError('upstream_unavailable', 'Try later')
},
},
workerId: 'worker-a',
nextLeaseId: () => 'lease-b',
leaseDurationMs: 30_000,
now: () => new Date('2026-07-27T12:00:00.000Z'),
random: () => 0.5,
retryBaseMs: 2_000,
retryMaximumMs: 10_000,
})
expect(result).toMatchObject({
outcome: 'retried',
errorCode: 'upstream_unavailable',
})
expect(adapter.retry).toHaveBeenCalledWith(
job().id,
'worker-a:lease-b',
{ code: 'upstream_unavailable', detailRedacted: 'Try later' },
new Date('2026-07-27T12:00:02.000Z'),
)
})
it.each([
new PermanentJobError('payload_invalid', 'Safe validation detail'),
new Error('secret-bearing unexpected detail'),
])('fails non-transient errors without retrying', async (error) => {
const adapter = store()
const result = await processNextJob({
store: adapter,
handlers: {
'safe-test': async () => {
throw error
},
},
workerId: 'worker-a',
nextLeaseId: () => 'lease-c',
leaseDurationMs: 30_000,
})
expect(result.outcome).toBe('failed')
expect(adapter.retry).not.toHaveBeenCalled()
expect(adapter.fail).toHaveBeenCalledOnce()
if (!(error instanceof PermanentJobError)) {
expect(adapter.fail).toHaveBeenCalledWith(
job().id,
'worker-a:lease-c',
expect.not.objectContaining({
detailRedacted: expect.stringContaining('secret-bearing'),
}),
)
}
})
it('marks an unknown job type as a permanent safe failure', async () => {
const adapter = store(job({ type: 'untrusted.command' }))
const result = await processNextJob({
store: adapter,
handlers: {},
workerId: 'worker-a',
nextLeaseId: () => 'lease-d',
leaseDurationMs: 30_000,
})
expect(result).toMatchObject({
outcome: 'failed',
errorCode: 'job_type_unsupported',
})
})
})
+339
View File
@@ -0,0 +1,339 @@
import { DomainError } from '@devrunbook/domain'
export type JobJsonValue =
| null
| boolean
| number
| string
| readonly JobJsonValue[]
| { readonly [key: string]: JobJsonValue }
export type JobState =
'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'
export interface JobRecord {
readonly id: string
readonly workspaceId: string | null
readonly type: string
readonly state: JobState
readonly idempotencyKey: string
readonly payload: JobJsonValue
readonly progress: JobJsonValue
readonly attemptCount: number
readonly maxAttempts: number
readonly leaseOwner: string | null
readonly leaseExpiresAt: Date | null
readonly availableAt: Date
readonly startedAt: Date | null
readonly finishedAt: Date | null
readonly errorCode: string | null
readonly errorDetailRedacted: string | null
readonly createdAt: Date
readonly updatedAt: Date
}
export interface EnqueueJobRequest {
readonly workspaceId: string | null
readonly type: string
readonly idempotencyKey: string
readonly payload: JobJsonValue
readonly maxAttempts?: number
readonly availableAt?: Date
}
export interface ClaimJobRequest {
readonly leaseOwner: string
readonly leaseDurationMs: number
readonly workspaceId?: string
}
export interface JobFailure {
readonly code: string
readonly detailRedacted: string
}
export interface JobStore {
enqueue(
request: EnqueueJobRequest,
): Promise<{ job: JobRecord; created: boolean }>
claim(request: ClaimJobRequest): Promise<JobRecord | null>
heartbeat(
jobId: string,
leaseOwner: string,
leaseDurationMs: number,
): Promise<boolean>
succeed(
jobId: string,
leaseOwner: string,
progress: JobJsonValue,
): Promise<boolean>
retry(
jobId: string,
leaseOwner: string,
failure: JobFailure,
availableAt: Date,
): Promise<boolean>
fail(jobId: string, leaseOwner: string, failure: JobFailure): Promise<boolean>
findForWorkspace(
workspaceId: string,
jobId: string,
): Promise<JobRecord | null>
}
export interface JobHandlerContext {
readonly signal: AbortSignal
heartbeat(): Promise<boolean>
}
export type JobHandler = (
job: JobRecord,
context: JobHandlerContext,
) => Promise<JobJsonValue>
export type JobHandlers = Readonly<Record<string, JobHandler>>
export class TransientJobError extends Error {
constructor(
readonly code: string,
readonly detailRedacted: string,
) {
super(detailRedacted)
this.name = 'TransientJobError'
}
}
export class PermanentJobError extends Error {
constructor(
readonly code: string,
readonly detailRedacted: string,
) {
super(detailRedacted)
this.name = 'PermanentJobError'
}
}
export interface ProcessNextJobDependencies {
readonly store: JobStore
readonly handlers: JobHandlers
readonly workerId: string
readonly nextLeaseId: () => string
readonly leaseDurationMs: number
readonly now?: () => Date
readonly random?: () => number
readonly retryBaseMs?: number
readonly retryMaximumMs?: number
readonly workspaceId?: string
}
export type ProcessJobResult =
| { readonly outcome: 'idle' }
| {
readonly outcome: 'succeeded' | 'retried' | 'failed' | 'lease-lost'
readonly jobId: string
readonly jobType: string
readonly errorCode?: string
}
const UNKNOWN_FAILURE: JobFailure = {
code: 'job_handler_failed',
detailRedacted: 'The job failed unexpectedly; inspect redacted server logs',
}
function assertToken(value: string, field: string, maximum: number): void {
if (value.length === 0 || value.length > maximum || value.trim() !== value) {
throw new DomainError(
`job_${field}_invalid`,
`${field} must contain 1 to ${maximum} characters without surrounding whitespace`,
)
}
}
export async function enqueueJob(
store: JobStore,
request: EnqueueJobRequest,
): Promise<{ job: JobRecord; created: boolean }> {
assertToken(request.type, 'type', 128)
assertToken(request.idempotencyKey, 'idempotency_key', 255)
const maximumAttempts = request.maxAttempts ?? 3
if (
!Number.isInteger(maximumAttempts) ||
maximumAttempts < 1 ||
maximumAttempts > 20
) {
throw new DomainError(
'job_max_attempts_invalid',
'maxAttempts must be an integer from 1 through 20',
)
}
return store.enqueue({ ...request, maxAttempts: maximumAttempts })
}
function retryAt(
attemptCount: number,
now: Date,
random: number,
baseMs: number,
maximumMs: number,
): Date {
const exponential = Math.min(
maximumMs,
baseMs * 2 ** Math.max(0, attemptCount - 1),
)
const boundedRandom = Math.max(0, Math.min(1, random))
const jittered = Math.round(exponential * (0.75 + boundedRandom * 0.5))
return new Date(now.getTime() + Math.min(maximumMs, jittered))
}
function classifiedFailure(error: unknown): {
failure: JobFailure
transient: boolean
} {
if (error instanceof TransientJobError) {
return {
failure: { code: error.code, detailRedacted: error.detailRedacted },
transient: true,
}
}
if (error instanceof PermanentJobError) {
return {
failure: { code: error.code, detailRedacted: error.detailRedacted },
transient: false,
}
}
return { failure: UNKNOWN_FAILURE, transient: false }
}
export async function processNextJob(
dependencies: ProcessNextJobDependencies,
): Promise<ProcessJobResult> {
if (dependencies.leaseDurationMs < 1_000) {
throw new DomainError(
'job_lease_duration_invalid',
'Job lease duration must be at least one second',
)
}
const leaseOwner = `${dependencies.workerId}:${dependencies.nextLeaseId()}`
const job = await dependencies.store.claim({
leaseOwner,
leaseDurationMs: dependencies.leaseDurationMs,
...(dependencies.workspaceId === undefined
? {}
: { workspaceId: dependencies.workspaceId }),
})
if (!job) return { outcome: 'idle' }
const handler = dependencies.handlers[job.type]
if (!handler) {
const failure = {
code: 'job_type_unsupported',
detailRedacted: 'No safe handler is registered for this job type',
}
const changed = await dependencies.store.fail(job.id, leaseOwner, failure)
return {
outcome: changed ? 'failed' : 'lease-lost',
jobId: job.id,
jobType: job.type,
errorCode: failure.code,
}
}
const abortController = new AbortController()
let heartbeatPending = false
let leaseLost = false
const heartbeat = async () => {
if (leaseLost) return false
try {
const retained = await dependencies.store.heartbeat(
job.id,
leaseOwner,
dependencies.leaseDurationMs,
)
if (!retained) {
leaseLost = true
abortController.abort()
}
return retained
} catch {
leaseLost = true
abortController.abort()
return false
}
}
const heartbeatTimer = setInterval(
() => {
if (heartbeatPending || leaseLost) return
heartbeatPending = true
void heartbeat().finally(() => {
heartbeatPending = false
})
},
Math.max(250, Math.floor(dependencies.leaseDurationMs / 3)),
)
heartbeatTimer.unref()
try {
const progress = await handler(job, {
signal: abortController.signal,
heartbeat,
})
if (leaseLost) {
return { outcome: 'lease-lost', jobId: job.id, jobType: job.type }
}
const changed = await dependencies.store.succeed(
job.id,
leaseOwner,
progress,
)
return {
outcome: changed ? 'succeeded' : 'lease-lost',
jobId: job.id,
jobType: job.type,
}
} catch (error) {
const { failure, transient } = classifiedFailure(error)
if (leaseLost) {
return { outcome: 'lease-lost', jobId: job.id, jobType: job.type }
}
if (transient && job.attemptCount < job.maxAttempts) {
const availableAt = retryAt(
job.attemptCount,
(dependencies.now ?? (() => new Date()))(),
(dependencies.random ?? Math.random)(),
dependencies.retryBaseMs ?? 1_000,
dependencies.retryMaximumMs ?? 60_000,
)
const changed = await dependencies.store.retry(
job.id,
leaseOwner,
failure,
availableAt,
)
return {
outcome: changed ? 'retried' : 'lease-lost',
jobId: job.id,
jobType: job.type,
errorCode: failure.code,
}
}
const exhaustedFailure =
transient && job.attemptCount >= job.maxAttempts
? {
code: 'job_retry_exhausted',
detailRedacted: `Retry limit reached after ${job.attemptCount} attempts (${failure.code})`,
}
: failure
const changed = await dependencies.store.fail(
job.id,
leaseOwner,
exhaustedFailure,
)
return {
outcome: changed ? 'failed' : 'lease-lost',
jobId: job.id,
jobType: job.type,
errorCode: exhaustedFailure.code,
}
} finally {
clearInterval(heartbeatTimer)
}
}
@@ -0,0 +1,96 @@
import { describe, expect, it, vi } from 'vitest'
import {
createPlaybookCollection,
listPlaybookCollections,
mutatePlaybookCollectionItem,
type PlaybookCollectionStore,
} from './playbook-collections'
const actor = {
userId: '00000000-0000-4000-8000-000000000001',
workspaceId: '00000000-0000-4000-8000-000000000002',
}
function store(
overrides: Partial<PlaybookCollectionStore> = {},
): PlaybookCollectionStore {
return {
list: vi.fn(async () => []),
create: vi.fn(async (input) => ({
id: '00000000-0000-4000-8000-000000000003',
name: input.name,
description: input.description,
itemCount: 0,
playbookIds: [],
createdAt: new Date('2026-07-27T12:00:00.000Z'),
updatedAt: new Date('2026-07-27T12:00:00.000Z'),
})),
mutateItem: vi.fn(async () => true),
...overrides,
}
}
describe('playbook collections', () => {
it('lists only through the actor workspace and creator scope', async () => {
const persistence = store()
await listPlaybookCollections(persistence, actor)
expect(persistence.list).toHaveBeenCalledWith({
workspaceId: actor.workspaceId,
createdBy: actor.userId,
})
})
it('normalizes bounded collection content before creation', async () => {
const persistence = store()
await createPlaybookCollection(persistence, {
actor,
name: ' Release readiness ',
description: ' Checks before shipping. ',
})
expect(persistence.create).toHaveBeenCalledWith({
workspaceId: actor.workspaceId,
createdBy: actor.userId,
name: 'Release readiness',
description: 'Checks before shipping.',
})
})
it.each([
['', 'collection_name_invalid'],
['x'.repeat(81), 'collection_name_invalid'],
['unsafe\u0000name', 'collection_name_invalid'],
])('rejects invalid name %#', async (name, code) => {
await expect(
createPlaybookCollection(store(), { actor, name }),
).rejects.toMatchObject({ code })
})
it('rejects invalid descriptions and duplicate personal names', async () => {
await expect(
createPlaybookCollection(store(), {
actor,
name: 'Safe',
description: 'x'.repeat(501),
}),
).rejects.toMatchObject({ code: 'collection_description_invalid' })
await expect(
createPlaybookCollection(store({ create: async () => 'duplicate' }), {
actor,
name: 'Safe',
}),
).rejects.toMatchObject({ code: 'collection_name_conflict' })
})
it('conflates substituted collections and inaccessible playbooks', async () => {
const persistence = store({ mutateItem: async () => false })
await expect(
mutatePlaybookCollectionItem(persistence, {
actor,
collectionId: '00000000-0000-4000-8000-000000000004',
playbookId: '00000000-0000-4000-8000-000000000005',
mutation: 'add',
}),
).rejects.toMatchObject({ code: 'collection_target_not_found' })
})
})
@@ -0,0 +1,134 @@
import { DomainError } from '@devrunbook/domain'
import type { ActorContext } from '../auth/workspace-authorization'
export interface PlaybookCollection {
readonly id: string
readonly name: string
readonly description: string
readonly itemCount: number
readonly playbookIds: readonly string[]
readonly createdAt: Date
readonly updatedAt: Date
}
export interface PlaybookCollectionStore {
list(input: {
readonly workspaceId: string
readonly createdBy: string
}): Promise<readonly PlaybookCollection[]>
create(input: {
readonly workspaceId: string
readonly createdBy: string
readonly name: string
readonly description: string
}): Promise<PlaybookCollection | 'duplicate'>
mutateItem(input: {
readonly workspaceId: string
readonly createdBy: string
readonly collectionId: string
readonly playbookId: string
readonly mutation: 'add' | 'remove'
}): Promise<boolean>
}
export interface CreatePlaybookCollectionInput {
readonly actor: Pick<ActorContext, 'userId' | 'workspaceId'>
readonly name: unknown
readonly description?: unknown
}
function normalizeName(value: unknown): string {
if (typeof value !== 'string') {
throw new DomainError(
'collection_name_invalid',
'Collection name must be text',
)
}
const name = value.trim()
const hasControlCharacter = [...name].some((character) => {
const code = character.codePointAt(0) ?? 0
return code <= 31 || code === 127
})
if (name.length < 1 || name.length > 80 || hasControlCharacter) {
throw new DomainError(
'collection_name_invalid',
'Collection name must contain 1 to 80 visible characters',
)
}
return name
}
function normalizeDescription(value: unknown): string {
if (value === undefined) return ''
if (typeof value !== 'string') {
throw new DomainError(
'collection_description_invalid',
'Collection description must be text',
)
}
const description = value.trim()
if (
description.length > 500 ||
[...description].some((character) => character.codePointAt(0) === 0)
) {
throw new DomainError(
'collection_description_invalid',
'Collection description must not exceed 500 characters',
)
}
return description
}
export function listPlaybookCollections(
store: PlaybookCollectionStore,
actor: Pick<ActorContext, 'userId' | 'workspaceId'>,
): Promise<readonly PlaybookCollection[]> {
return store.list({
workspaceId: actor.workspaceId,
createdBy: actor.userId,
})
}
export async function createPlaybookCollection(
store: PlaybookCollectionStore,
input: CreatePlaybookCollectionInput,
): Promise<PlaybookCollection> {
const created = await store.create({
workspaceId: input.actor.workspaceId,
createdBy: input.actor.userId,
name: normalizeName(input.name),
description: normalizeDescription(input.description),
})
if (created === 'duplicate') {
throw new DomainError(
'collection_name_conflict',
'A personal collection with this name already exists',
)
}
return created
}
export async function mutatePlaybookCollectionItem(
store: PlaybookCollectionStore,
input: {
readonly actor: Pick<ActorContext, 'userId' | 'workspaceId'>
readonly collectionId: string
readonly playbookId: string
readonly mutation: 'add' | 'remove'
},
): Promise<void> {
const accessible = await store.mutateItem({
workspaceId: input.actor.workspaceId,
createdBy: input.actor.userId,
collectionId: input.collectionId,
playbookId: input.playbookId,
mutation: input.mutation,
})
if (!accessible) {
throw new DomainError(
'collection_target_not_found',
'Collection or playbook was not found in the authorized workspace',
)
}
}
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from 'vitest'
import {
mutatePlaybookFavorite,
type PlaybookFavoriteStore,
} from './playbook-favorites'
const actor = {
userId: '00000000-0000-4000-8000-000000000001',
workspaceId: '00000000-0000-4000-8000-000000000002',
}
const playbookId = '00000000-0000-4000-8000-000000000003'
describe('mutatePlaybookFavorite', () => {
it.each(['add', 'remove'] as const)(
'passes only authorized actor identity to the %s mutation',
async (mutation) => {
const mutateFavorite = vi.fn(async () => true)
const store: PlaybookFavoriteStore = { mutateFavorite }
await expect(
mutatePlaybookFavorite(store, { actor, playbookId, mutation }),
).resolves.toBeUndefined()
expect(mutateFavorite).toHaveBeenCalledWith({
...actor,
playbookId,
mutation,
})
},
)
it('uses the same not-found failure for missing and inaccessible targets', async () => {
const store: PlaybookFavoriteStore = {
mutateFavorite: async () => false,
}
await expect(
mutatePlaybookFavorite(store, { actor, playbookId, mutation: 'add' }),
).rejects.toMatchObject({ code: 'playbook_not_found' })
})
})
@@ -0,0 +1,43 @@
import { DomainError } from '@devrunbook/domain'
import type { ActorContext } from '../auth/workspace-authorization'
export type PlaybookFavoriteMutation = 'add' | 'remove'
export interface PlaybookFavoriteMutationInput {
readonly actor: Pick<ActorContext, 'userId' | 'workspaceId'>
readonly playbookId: string
readonly mutation: PlaybookFavoriteMutation
}
/**
* The persistence adapter must check target accessibility and apply the
* mutation in one transaction. A false result deliberately conflates missing
* and inaccessible playbooks.
*/
export interface PlaybookFavoriteStore {
mutateFavorite(input: {
readonly workspaceId: string
readonly userId: string
readonly playbookId: string
readonly mutation: PlaybookFavoriteMutation
}): Promise<boolean>
}
export async function mutatePlaybookFavorite(
store: PlaybookFavoriteStore,
input: PlaybookFavoriteMutationInput,
): Promise<void> {
const accessible = await store.mutateFavorite({
workspaceId: input.actor.workspaceId,
userId: input.actor.userId,
playbookId: input.playbookId,
mutation: input.mutation,
})
if (!accessible) {
throw new DomainError(
'playbook_not_found',
'Playbook was not found in the authorized workspace',
)
}
}
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest'
import type { OperationsActor, OperationsStore } from './operations'
import {
authorizeOperations,
listOperationsAuditEvents,
listOperationsJobs,
retryOperationsJob,
} from './operations'
function actor(
instanceRole: OperationsActor['instanceRole'],
workspaceRole: OperationsActor['workspaceRole'],
): OperationsActor {
return {
userId: 'user-a',
instanceRole,
workspaceId: workspaceRole ? 'workspace-a' : null,
workspaceRole,
}
}
function store(): OperationsStore {
return {
listJobs: vi.fn(async () => ({ items: [], nextCursor: null })),
findJob: vi.fn(async () => null),
retryJob: vi.fn(async (): Promise<'retried'> => 'retried'),
listAuditEvents: vi.fn(async () => ({ items: [], nextCursor: null })),
}
}
describe('operations authorization', () => {
it.each(['instance_owner', 'instance_admin'] as const)(
'grants %s instance-wide read and retry without workspace membership',
(role) => {
expect(authorizeOperations(actor(role, null), 'read')).toEqual({
kind: 'instance',
})
expect(authorizeOperations(actor(role, null), 'retry')).toEqual({
kind: 'instance',
})
},
)
it.each(['editor', 'owner'] as const)(
'scopes %s access to its workspace and permits retry',
(role) => {
expect(authorizeOperations(actor('user', role), 'read')).toEqual({
kind: 'workspace',
workspaceId: 'workspace-a',
})
expect(authorizeOperations(actor('user', role), 'retry')).toEqual({
kind: 'workspace',
workspaceId: 'workspace-a',
})
},
)
it('allows viewer reads but denies retry and cross-workspace audit filters', async () => {
const target = store()
await expect(
listOperationsJobs(target, actor('user', 'viewer')),
).resolves.toBeDefined()
await expect(
retryOperationsJob(target, actor('user', 'viewer'), 'job-a'),
).rejects.toMatchObject({ code: 'operations_access_denied' })
expect(() =>
listOperationsAuditEvents(target, actor('user', 'viewer'), {
workspaceId: 'workspace-b',
}),
).toThrow('Operations access is not permitted')
})
it('denies an ordinary user without workspace membership', () => {
expect(() => authorizeOperations(actor('user', null), 'read')).toThrow(
'Operations access is not permitted',
)
})
it('maps transactional retry outcomes to safe domain errors', async () => {
const target = store()
vi.mocked(target.retryJob).mockResolvedValueOnce('not-found')
await expect(
retryOperationsJob(target, actor('user', 'owner'), 'foreign-job'),
).rejects.toMatchObject({ code: 'operations_job_not_found' })
vi.mocked(target.retryJob).mockResolvedValueOnce('not-retryable')
await expect(
retryOperationsJob(target, actor('user', 'owner'), 'running-job'),
).rejects.toMatchObject({ code: 'operations_job_not_retryable' })
})
})
@@ -0,0 +1,206 @@
import { DomainError } from '@devrunbook/domain'
import type {
InstanceRole,
WorkspaceRole,
} from '../auth/workspace-authorization'
import type { JobJsonValue, JobRecord, JobState } from '../jobs/job-queue'
export interface OperationsActor {
readonly userId: string
readonly instanceRole: InstanceRole
readonly workspaceId: string | null
readonly workspaceRole: WorkspaceRole | null
}
export interface OperationsActorLookup {
findOperationsActor(userId: string): Promise<OperationsActor | null>
}
export type OperationsScope =
| { readonly kind: 'instance' }
| { readonly kind: 'workspace'; readonly workspaceId: string }
export interface OperationsJob {
readonly id: string
readonly workspaceId: string | null
readonly type: string
readonly state: JobState
readonly progress: JobJsonValue
readonly attemptCount: number
readonly maxAttempts: number
readonly errorCode: string | null
readonly errorDetail: string | null
readonly retryable: boolean
readonly createdAt: Date
readonly updatedAt: Date
}
export interface AuditEventRecord {
readonly id: string
readonly occurredAt: Date
readonly actorUserId: string | null
readonly workspaceId: string | null
readonly action: string
readonly resourceType: string
readonly resourceId: string | null
readonly outcome: 'success' | 'denied' | 'failed'
readonly metadata: Readonly<Record<string, unknown>>
}
export interface OperationsPage<T> {
readonly items: readonly T[]
readonly nextCursor: string | null
}
export interface OperationsStore {
listJobs(request: {
scope: OperationsScope
cursor: string | null
limit: number
state?: JobState
}): Promise<OperationsPage<OperationsJob>>
findJob(scope: OperationsScope, jobId: string): Promise<OperationsJob | null>
retryJob(request: {
scope: OperationsScope
jobId: string
actorUserId: string
workspaceId: string | null
}): Promise<'retried' | 'not-found' | 'not-retryable'>
listAuditEvents(request: {
scope: OperationsScope
cursor: string | null
limit: number
action?: string
workspaceId?: string
}): Promise<OperationsPage<AuditEventRecord>>
}
export function operationsActor(context: OperationsActor): OperationsActor {
return context
}
function deny(): never {
throw new DomainError(
'operations_access_denied',
'Operations access is not permitted',
)
}
export function authorizeOperations(
actor: OperationsActor,
action: 'read' | 'retry',
): OperationsScope {
if (
actor.instanceRole === 'instance_owner' ||
actor.instanceRole === 'instance_admin'
) {
return { kind: 'instance' }
}
if (!actor.workspaceId || !actor.workspaceRole) return deny()
if (
action === 'retry' &&
actor.workspaceRole !== 'editor' &&
actor.workspaceRole !== 'owner'
) {
return deny()
}
return { kind: 'workspace', workspaceId: actor.workspaceId }
}
function validLimit(limit: number): number {
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
throw new DomainError(
'operations_limit_invalid',
'Operations page limit must be an integer from 1 through 100',
)
}
return limit
}
export function listOperationsJobs(
store: OperationsStore,
actor: OperationsActor,
request: { cursor?: string; limit?: number; state?: JobState } = {},
) {
return store.listJobs({
scope: authorizeOperations(actor, 'read'),
cursor: request.cursor ?? null,
limit: validLimit(request.limit ?? 25),
...(request.state ? { state: request.state } : {}),
})
}
export function getOperationsJob(
store: OperationsStore,
actor: OperationsActor,
jobId: string,
) {
return store.findJob(authorizeOperations(actor, 'read'), jobId)
}
export async function retryOperationsJob(
store: OperationsStore,
actor: OperationsActor,
jobId: string,
): Promise<void> {
const result = await store.retryJob({
scope: authorizeOperations(actor, 'retry'),
jobId,
actorUserId: actor.userId,
workspaceId: actor.workspaceId,
})
if (result === 'not-found') {
throw new DomainError('operations_job_not_found', 'Job was not found')
}
if (result === 'not-retryable') {
throw new DomainError(
'operations_job_not_retryable',
'Only retryable terminal jobs can be retried',
)
}
}
export function listOperationsAuditEvents(
store: OperationsStore,
actor: OperationsActor,
request: {
cursor?: string
limit?: number
action?: string
workspaceId?: string
} = {},
) {
const scope = authorizeOperations(actor, 'read')
if (
scope.kind === 'workspace' &&
request.workspaceId !== undefined &&
request.workspaceId !== scope.workspaceId
) {
return deny()
}
return store.listAuditEvents({
scope,
cursor: request.cursor ?? null,
limit: validLimit(request.limit ?? 25),
...(request.action ? { action: request.action } : {}),
...(request.workspaceId ? { workspaceId: request.workspaceId } : {}),
})
}
export function projectOperationsJob(job: JobRecord): OperationsJob {
return {
id: job.id,
workspaceId: job.workspaceId,
type: job.type,
state: job.state,
progress: job.progress,
attemptCount: job.attemptCount,
maxAttempts: job.maxAttempts,
errorCode: job.errorCode,
errorDetail: job.errorDetailRedacted,
retryable: false,
createdAt: job.createdAt,
updatedAt: job.updatedAt,
}
}
@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from 'vitest'
import {
recordSimpleFlowMetric,
type ProductMetricStore,
} from './product-metrics'
describe('simple-flow product metrics', () => {
it('stores only bounded privacy-safe dimensions and buckets duration', async () => {
const recordSimpleFlowMetricStore = vi.fn(async () => undefined)
const store: ProductMetricStore = {
recordSimpleFlowMetric: recordSimpleFlowMetricStore,
}
await recordSimpleFlowMetric(
store,
{
userId: 'user-1',
workspaceId: 'workspace-1',
workspaceRole: 'editor',
instanceRole: 'user',
},
{
event: 'draft_created',
taskSlug: 'root-cause-bugfix',
durationMs: 45_000,
},
)
expect(recordSimpleFlowMetricStore).toHaveBeenCalledWith({
actorUserId: 'user-1',
workspaceId: 'workspace-1',
event: 'draft_created',
taskSlug: 'root-cause-bugfix',
durationBucket: '30s-2m',
})
})
it('rejects unbounded or non-slug dimensions', async () => {
const store: ProductMetricStore = {
recordSimpleFlowMetric: vi.fn(async () => undefined),
}
expect(() =>
recordSimpleFlowMetric(
store,
{
userId: 'user-1',
workspaceId: 'workspace-1',
workspaceRole: 'editor',
instanceRole: 'user',
},
{ event: 'viewed', taskSlug: 'raw task text with spaces' },
),
).toThrowError(expect.objectContaining({ code: 'product_metric_invalid' }))
})
})
@@ -0,0 +1,57 @@
import { DomainError } from '@devrunbook/domain'
import type { ActorContext } from '../auth/workspace-authorization'
export const simpleFlowEvents = [
'viewed',
'draft_created',
'advanced_opened',
'generation_requested',
] as const
export type SimpleFlowEvent = (typeof simpleFlowEvents)[number]
export interface ProductMetricStore {
recordSimpleFlowMetric(input: {
readonly actorUserId: string
readonly workspaceId: string
readonly event: SimpleFlowEvent
readonly taskSlug: string | null
readonly durationBucket: string | null
}): Promise<void>
}
function durationBucket(durationMs: number | null): string | null {
if (durationMs === null) return null
if (!Number.isFinite(durationMs) || durationMs < 0 || durationMs > 86_400_000)
throw new DomainError('product_metric_invalid', 'Duration is invalid')
if (durationMs < 30_000) return 'under-30s'
if (durationMs < 120_000) return '30s-2m'
if (durationMs < 300_000) return '2m-5m'
return 'over-5m'
}
export function recordSimpleFlowMetric(
store: ProductMetricStore,
actor: ActorContext,
input: {
readonly event: SimpleFlowEvent
readonly taskSlug?: string
readonly durationMs?: number
},
): Promise<void> {
if (!actor.workspaceId || !actor.userId) {
throw new DomainError('workspace_access_denied', 'Workspace access denied')
}
const taskSlug = input.taskSlug?.trim() || null
if (taskSlug && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(taskSlug)) {
throw new DomainError('product_metric_invalid', 'Task slug is invalid')
}
return store.recordSimpleFlowMetric({
actorUserId: actor.userId,
workspaceId: actor.workspaceId,
event: input.event,
taskSlug,
durationBucket: durationBucket(input.durationMs ?? null),
})
}
@@ -0,0 +1,73 @@
import { describe, expect, it, vi } from 'vitest'
import {
importBuiltInPlaybooks,
type BuiltInPlaybookImportRecord,
type BuiltInPlaybookImportStore,
} from './import-built-in-playbooks'
function records(count = 28): BuiltInPlaybookImportRecord[] {
return Array.from({ length: count }, (_, index) => ({
logicalId: `playbook-${index}`,
slug: `playbook-${index}`,
namespace: 'builtin',
sourceType: 'built_in',
semanticVersion: '1.0.0',
lifecycle: 'reviewed',
packageApiVersion: 'devrunbook.io/v1alpha1',
title: `Playbook ${index}`,
summary: 'Summary',
category: 'testing',
riskTier: 'low',
packageJson: {},
templateText: 'Prompt\n',
contentDigest: index.toString(16).padStart(64, '0'),
searchProjection: { searchText: `Playbook ${index}` },
}))
}
describe('built-in playbook import', () => {
it('requires the complete canonical catalog before persistence', async () => {
const store: BuiltInPlaybookImportStore = {
importBuiltIns: vi.fn(),
}
await expect(
importBuiltInPlaybooks(store, records(27)),
).rejects.toMatchObject({ code: 'catalog_import_incomplete' })
expect(store.importBuiltIns).not.toHaveBeenCalled()
})
it('returns safe inserted and unchanged counts', async () => {
const store: BuiltInPlaybookImportStore = {
importBuiltIns: vi.fn().mockResolvedValue({
total: 28,
insertedPlaybooks: 0,
insertedVersions: 2,
unchangedVersions: 26,
}),
}
await expect(importBuiltInPlaybooks(store, records())).resolves.toEqual({
total: 28,
insertedPlaybooks: 0,
insertedVersions: 2,
unchangedVersions: 26,
})
})
it('rejects an inconsistent persistence result', async () => {
const store: BuiltInPlaybookImportStore = {
importBuiltIns: vi.fn().mockResolvedValue({
total: 28,
insertedPlaybooks: 0,
insertedVersions: 1,
unchangedVersions: 26,
}),
}
await expect(
importBuiltInPlaybooks(store, records()),
).rejects.toMatchObject({ code: 'catalog_import_incomplete' })
})
})
@@ -0,0 +1,63 @@
import { DomainError } from '@devrunbook/domain'
export const requiredBuiltInPlaybookCount = 28
export interface BuiltInPlaybookImportRecord {
readonly logicalId: string
readonly slug: string
readonly namespace: 'builtin'
readonly sourceType: 'built_in'
readonly semanticVersion: string
readonly lifecycle: string
readonly packageApiVersion: string
readonly title: string
readonly summary: string
readonly category: string
readonly riskTier: string
readonly packageJson: unknown
readonly templateText: string
readonly contentDigest: string
readonly searchProjection: { readonly searchText: string }
}
export interface BuiltInPlaybookImportResult {
readonly total: number
readonly insertedPlaybooks: number
readonly insertedVersions: number
readonly unchangedVersions: number
}
export interface BuiltInPlaybookImportStore {
importBuiltIns(
records: readonly BuiltInPlaybookImportRecord[],
): Promise<BuiltInPlaybookImportResult>
}
/**
* Synchronize the canonical built-in catalog without interpreting or executing
* any package content. Structural and semantic validation remains the content
* loader's responsibility before records reach this boundary.
*/
export async function importBuiltInPlaybooks(
store: BuiltInPlaybookImportStore,
records: readonly BuiltInPlaybookImportRecord[],
): Promise<BuiltInPlaybookImportResult> {
if (records.length !== requiredBuiltInPlaybookCount) {
throw new DomainError(
'catalog_import_incomplete',
`Built-in import requires ${requiredBuiltInPlaybookCount} playbooks; received ${records.length}`,
)
}
const result = await store.importBuiltIns(records)
if (
result.total !== requiredBuiltInPlaybookCount ||
result.insertedVersions + result.unchangedVersions !== result.total
) {
throw new DomainError(
'catalog_import_incomplete',
'Built-in persistence returned an incomplete import result',
)
}
return result
}
@@ -0,0 +1,247 @@
import { createHash } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import type { WorkspaceAuthorizationLookup } from '../auth/workspace-authorization'
import {
createPrivatePlaybookDraft,
formatPrivatePlaybookDraftEtag,
getPrivatePlaybookDraft,
updatePrivatePlaybookDraft,
type PrivatePlaybookDraft,
type PrivatePlaybookDraftDependencies,
type ValidatedPrivatePlaybookPackage,
} from './private-playbook-drafts'
const workspaceId = '00000000-0000-4000-8000-000000000001'
const userId = '00000000-0000-4000-8000-000000000002'
const versionId = '00000000-0000-4000-8000-000000000003'
const playbookId = '00000000-0000-4000-8000-000000000004'
const actor = { userId }
function digest(value: string): string {
return createHash('sha256').update(value).digest('hex')
}
function packageValue(
lifecycle: ValidatedPrivatePlaybookPackage['lifecycle'] = 'draft',
): ValidatedPrivatePlaybookPackage {
const manifest = new TextEncoder().encode('kind: PlaybookPackage\n')
const template = new TextEncoder().encode('# Mission\n')
return {
logicalId: 'private-example',
slug: 'private-example',
semanticVersion: '0.1.0',
lifecycle,
packageApiVersion: 'devrunbook.io/v1alpha1',
title: 'Private example',
summary: 'A private authoring example with governed validation.',
category: 'Authoring',
riskTier: 'low',
packageJson: { kind: 'PlaybookPackage' },
templateText: '# Mission\n',
contentDigest: digest(`package-${lifecycle}`),
searchText: 'Private example\nAuthoring',
files: [
{
path: 'playbook.yaml',
role: 'manifest',
mediaType: 'application/yaml',
content: manifest,
sizeBytes: manifest.byteLength,
sha256: createHash('sha256').update(manifest).digest('hex'),
digest: true,
exportByDefault: true,
},
{
path: 'prompt.md',
role: 'template',
mediaType: 'text/markdown',
content: template,
sizeBytes: template.byteLength,
sha256: createHash('sha256').update(template).digest('hex'),
digest: true,
exportByDefault: true,
},
],
}
}
function draft(
lifecycle: ValidatedPrivatePlaybookPackage['lifecycle'] = 'draft',
): PrivatePlaybookDraft {
const value = packageValue(lifecycle)
return {
playbookId,
versionId,
logicalId: value.logicalId,
slug: value.slug,
semanticVersion: value.semanticVersion,
title: value.title,
lifecycle,
draftRevision: 1,
draftDigest: value.contentDigest,
publishedAt: null,
updatedAt: '2026-07-27T12:00:00.000Z',
packageApiVersion: value.packageApiVersion,
summary: value.summary,
category: value.category,
riskTier: value.riskTier,
packageJson: value.packageJson,
templateText: value.templateText,
files: value.files,
}
}
function dependencies(role: 'viewer' | 'editor' = 'editor') {
let current = draft()
const authorization: WorkspaceAuthorizationLookup = {
findWorkspaceAuthorization: vi.fn(async () => ({
userId,
instanceRole: 'user' as const,
workspaceId,
workspaceRole: role,
userStatus: 'active' as const,
})),
}
const value: PrivatePlaybookDraftDependencies = {
authorization,
now: () => new Date('2026-07-27T12:00:00.000Z'),
store: {
listDraftsForWorkspace: vi.fn(async () => [current]),
findVersionForWorkspace: vi.fn(async (workspace, id) =>
workspace === workspaceId && id === versionId ? current : null,
),
createDraft: vi.fn(async (request) => {
current = {
...draft(),
logicalId: request.package.logicalId,
slug: request.package.slug,
}
return current
}),
replaceDraft: vi.fn(async (request) => {
if (
request.expectedRevision !== current.draftRevision ||
request.expectedDigest !== current.draftDigest
) {
throw Object.assign(new Error('conflict'), {
code: 'private_playbook_draft_conflict',
})
}
current = {
...current,
lifecycle: request.package.lifecycle,
draftRevision: current.draftRevision + 1,
draftDigest: request.package.contentDigest,
}
return current
}),
},
}
return value
}
describe('private playbook draft use cases', () => {
it('creates a workspace-namespaced draft and returns a strong ETag', async () => {
const deps = dependencies()
const result = await createPrivatePlaybookDraft(deps, {
actor,
workspaceId,
package: packageValue(),
})
expect(deps.store.createDraft).toHaveBeenCalledWith(
expect.objectContaining({
workspaceId,
createdBy: userId,
namespace: `private.${workspaceId}`,
}),
)
expect(result.etag).toBe(
formatPrivatePlaybookDraftEtag(1, packageValue().contentDigest),
)
})
it('enforces editor authorization and draft lifecycle on create', async () => {
await expect(
createPrivatePlaybookDraft(dependencies('viewer'), {
actor,
workspaceId,
package: packageValue(),
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
createPrivatePlaybookDraft(dependencies(), {
actor,
workspaceId,
package: packageValue('reviewed'),
}),
).rejects.toMatchObject({ code: 'private_playbook_package_invalid' })
})
it('loads only through workspace scope and advances CAS revision', async () => {
const deps = dependencies()
const before = await getPrivatePlaybookDraft(deps, {
actor,
workspaceId,
versionId,
})
const updated = await updatePrivatePlaybookDraft(deps, {
actor,
workspaceId,
versionId,
expectedEtag: before.etag,
package: packageValue('reviewed'),
})
expect(updated.draft.draftRevision).toBe(2)
expect(updated.draft.lifecycle).toBe('reviewed')
await expect(
updatePrivatePlaybookDraft(deps, {
actor,
workspaceId,
versionId,
expectedEtag: before.etag,
package: packageValue('reviewed'),
}),
).rejects.toMatchObject({ code: 'private_playbook_draft_conflict' })
})
it('rejects malformed ETags, unsafe paths and battle-tested self-claims', async () => {
const deps = dependencies()
await expect(
updatePrivatePlaybookDraft(deps, {
actor,
workspaceId,
versionId,
expectedEtag: 'weak',
package: packageValue(),
}),
).rejects.toMatchObject({ code: 'private_playbook_etag_invalid' })
await expect(
updatePrivatePlaybookDraft(deps, {
actor,
workspaceId,
versionId,
expectedEtag: formatPrivatePlaybookDraftEtag(
1,
packageValue().contentDigest,
),
package: packageValue('battle-tested'),
}),
).rejects.toMatchObject({ code: 'private_playbook_package_invalid' })
const unsafe = packageValue()
await expect(
createPrivatePlaybookDraft(deps, {
actor,
workspaceId,
package: {
...unsafe,
files: [
unsafe.files[0]!,
{ ...unsafe.files[1]!, path: '../prompt.md' },
],
},
}),
).rejects.toMatchObject({ code: 'private_playbook_package_invalid' })
})
})
@@ -0,0 +1,403 @@
import { createHash } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
const sha256Pattern = /^[a-f0-9]{64}$/u
const semverPattern =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u
const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u
const maximumPackageFiles = 201
const maximumFileBytes = 1024 * 1024
const maximumPackageBytes = 10 * 1024 * 1024
export type PrivatePlaybookLifecycle =
'draft' | 'reviewed' | 'validated' | 'battle-tested' | 'deprecated'
export type PlaybookPackageFileRole =
| 'manifest'
| 'template'
| 'partial'
| 'documentation'
| 'changelog'
| 'example'
| 'evaluation'
| 'resource'
| 'run-pack-resource'
export interface ValidatedPrivatePlaybookFile {
readonly path: string
readonly role: PlaybookPackageFileRole
readonly mediaType: string
readonly content: Uint8Array
readonly sizeBytes: number
readonly sha256: string
readonly digest: boolean
readonly exportByDefault: boolean
}
export interface ValidatedPrivatePlaybookPackage {
readonly logicalId: string
readonly slug: string
readonly semanticVersion: string
readonly lifecycle: PrivatePlaybookLifecycle
readonly packageApiVersion: string
readonly title: string
readonly summary: string
readonly category: string
readonly riskTier: 'low' | 'moderate' | 'high' | 'critical'
readonly packageJson: unknown
readonly templateText: string
readonly contentDigest: string
readonly searchText: string
readonly files: readonly ValidatedPrivatePlaybookFile[]
}
export interface PrivatePlaybookDraftSummary {
readonly playbookId: string
readonly versionId: string
readonly slug: string
readonly semanticVersion: string
readonly title: string
readonly lifecycle: PrivatePlaybookLifecycle
readonly draftRevision: number
readonly draftDigest: string
readonly publishedAt: string | null
readonly updatedAt: string
}
export interface PrivatePlaybookDraft extends PrivatePlaybookDraftSummary {
readonly logicalId: string
readonly packageApiVersion: string
readonly summary: string
readonly category: string
readonly riskTier: 'low' | 'moderate' | 'high' | 'critical'
readonly packageJson: unknown
readonly templateText: string
readonly files: readonly ValidatedPrivatePlaybookFile[]
}
export interface PrivatePlaybookDraftStore {
listDraftsForWorkspace(
workspaceId: string,
): Promise<readonly PrivatePlaybookDraftSummary[]>
findVersionForWorkspace(
workspaceId: string,
versionId: string,
): Promise<PrivatePlaybookDraft | null>
createDraft(request: {
readonly workspaceId: string
readonly createdBy: string
readonly namespace: string
readonly package: ValidatedPrivatePlaybookPackage
readonly now: Date
}): Promise<PrivatePlaybookDraft>
replaceDraft(request: {
readonly workspaceId: string
readonly versionId: string
readonly updatedBy: string
readonly expectedRevision: number
readonly expectedDigest: string
readonly package: ValidatedPrivatePlaybookPackage
readonly now: Date
}): Promise<PrivatePlaybookDraft | null>
}
export interface PrivatePlaybookDraftDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly store: PrivatePlaybookDraftStore
readonly now: () => Date
}
export interface PrivatePlaybookActorRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
}
export interface GetPrivatePlaybookDraftRequest extends PrivatePlaybookActorRequest {
readonly versionId: string
}
export interface UpdatePrivatePlaybookDraftRequest extends GetPrivatePlaybookDraftRequest {
readonly expectedEtag: string
readonly package: ValidatedPrivatePlaybookPackage
}
function invalid(path: string, message: string): never {
throw new DomainError(
'private_playbook_package_invalid',
'Private playbook package is invalid',
{
issues: [
{
path,
code: 'invalid',
message,
remediation: message,
},
],
},
)
}
function requiredText(value: string, path: string, maximum: number): string {
const normalized = value.trim()
if (
normalized.length === 0 ||
normalized.length > maximum ||
[...normalized].some((character) => character.charCodeAt(0) < 0x20)
) {
invalid(path, `Use 1 to ${maximum} printable characters.`)
}
return normalized
}
function requiredMultilineText(
value: string,
path: string,
maximum: number,
): string {
if (
value.trim().length === 0 ||
value.length > maximum ||
[...value].some((character) => {
const code = character.charCodeAt(0)
return code < 0x20 && character !== '\n' && character !== '\t'
})
) {
invalid(path, `Use 1 to ${maximum} UTF-8 text characters.`)
}
return value
}
function validatedPackage(
value: ValidatedPrivatePlaybookPackage,
): ValidatedPrivatePlaybookPackage {
const logicalId = requiredText(value.logicalId, '/metadata/id', 160)
const slug = requiredText(value.slug, '/metadata/slug', 120)
if (!slugPattern.test(slug))
invalid('/metadata/slug', 'Use a kebab-case slug.')
if (!semverPattern.test(value.semanticVersion)) {
invalid('/metadata/version', 'Use a valid Semantic Version.')
}
if (!sha256Pattern.test(value.contentDigest)) {
invalid('/contentDigest', 'Use a lowercase SHA-256 package digest.')
}
if (value.files.length < 2 || value.files.length > maximumPackageFiles) {
invalid(
'/files',
`Declare between 2 and ${maximumPackageFiles} package files.`,
)
}
if (!value.files.some((file) => file.path === 'playbook.yaml')) {
invalid('/files', 'Include playbook.yaml in the persisted file inventory.')
}
const paths = new Set<string>()
let totalBytes = 0
const files = value.files.map((file, index) => {
const path = requiredText(file.path, `/files/${index}/path`, 500)
if (
path.startsWith('/') ||
path.includes('\\') ||
path
.split('/')
.some((part) => part === '' || part === '.' || part === '..')
) {
invalid(`/files/${index}/path`, 'Use a normalized relative POSIX path.')
}
const collisionKey = path.toLowerCase()
if (paths.has(collisionKey)) {
invalid(
`/files/${index}/path`,
'Package paths must be unique ignoring case.',
)
}
paths.add(collisionKey)
if (
!Number.isSafeInteger(file.sizeBytes) ||
file.sizeBytes < 0 ||
file.sizeBytes !== file.content.byteLength ||
file.sizeBytes > maximumFileBytes
) {
invalid(`/files/${index}/sizeBytes`, 'File size is invalid.')
}
if (!sha256Pattern.test(file.sha256)) {
invalid(`/files/${index}/sha256`, 'Use a lowercase SHA-256 file digest.')
}
if (
createHash('sha256').update(file.content).digest('hex') !== file.sha256
) {
invalid(
`/files/${index}/sha256`,
'File digest must match the exact bytes.',
)
}
totalBytes += file.sizeBytes
return Object.freeze({ ...file, path, content: file.content.slice() })
})
if (totalBytes > maximumPackageBytes) {
invalid('/files', 'Expanded package content exceeds 10 MiB.')
}
return Object.freeze({
...value,
logicalId,
slug,
packageApiVersion: requiredText(value.packageApiVersion, '/apiVersion', 80),
title: requiredText(value.title, '/metadata/title', 200),
summary: requiredText(value.summary, '/metadata/summary', 500),
category: requiredText(value.category, '/metadata/category', 100),
templateText: requiredMultilineText(
value.templateText,
'/spec/template/main',
2_097_152,
),
searchText: requiredMultilineText(value.searchText, '/searchText', 65_536),
files: Object.freeze(files),
})
}
async function authorize(
dependencies: PrivatePlaybookDraftDependencies,
request: PrivatePlaybookActorRequest,
action: 'read' | 'write',
): Promise<string> {
const context = await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action,
})
return context.userId
}
function notFound(): never {
throw new DomainError(
'private_playbook_not_found',
'Private playbook not found',
)
}
export function formatPrivatePlaybookDraftEtag(
revision: number,
digest: string,
): string {
if (
!Number.isSafeInteger(revision) ||
revision < 1 ||
!sha256Pattern.test(digest)
) {
throw new TypeError('Private playbook draft ETag input is invalid')
}
return `"playbook-draft:${revision}:${digest}"`
}
export function parsePrivatePlaybookDraftEtag(value: string): {
readonly revision: number
readonly digest: string
} {
const match = /^"playbook-draft:([1-9]\d*):([a-f0-9]{64})"$/u.exec(value)
const revision = match ? Number(match[1]) : Number.NaN
if (!match || !Number.isSafeInteger(revision)) {
throw new DomainError(
'private_playbook_etag_invalid',
'A valid current private playbook draft ETag is required',
)
}
return { revision, digest: match[2]! }
}
export async function listPrivatePlaybookDrafts(
dependencies: PrivatePlaybookDraftDependencies,
request: PrivatePlaybookActorRequest,
): Promise<readonly PrivatePlaybookDraftSummary[]> {
await authorize(dependencies, request, 'read')
return dependencies.store.listDraftsForWorkspace(request.workspaceId)
}
export async function getPrivatePlaybookDraft(
dependencies: PrivatePlaybookDraftDependencies,
request: GetPrivatePlaybookDraftRequest,
): Promise<{ readonly draft: PrivatePlaybookDraft; readonly etag: string }> {
await authorize(dependencies, request, 'read')
const draft = await dependencies.store.findVersionForWorkspace(
request.workspaceId,
request.versionId,
)
if (!draft) notFound()
return {
draft,
etag: formatPrivatePlaybookDraftEtag(
draft.draftRevision,
draft.draftDigest,
),
}
}
export async function createPrivatePlaybookDraft(
dependencies: PrivatePlaybookDraftDependencies,
request: PrivatePlaybookActorRequest & {
readonly package: ValidatedPrivatePlaybookPackage
},
): Promise<{ readonly draft: PrivatePlaybookDraft; readonly etag: string }> {
const createdBy = await authorize(dependencies, request, 'write')
const packageValue = validatedPackage(request.package)
if (packageValue.lifecycle !== 'draft') {
invalid('/metadata/lifecycle', 'A new private version must start as draft.')
}
const draft = await dependencies.store.createDraft({
workspaceId: request.workspaceId,
createdBy,
namespace: `private.${request.workspaceId}`,
package: packageValue,
now: dependencies.now(),
})
return {
draft,
etag: formatPrivatePlaybookDraftEtag(
draft.draftRevision,
draft.draftDigest,
),
}
}
export async function updatePrivatePlaybookDraft(
dependencies: PrivatePlaybookDraftDependencies,
request: UpdatePrivatePlaybookDraftRequest,
): Promise<{ readonly draft: PrivatePlaybookDraft; readonly etag: string }> {
const updatedBy = await authorize(dependencies, request, 'write')
const expected = parsePrivatePlaybookDraftEtag(request.expectedEtag)
const packageValue = validatedPackage(request.package)
if (packageValue.lifecycle === 'battle-tested') {
invalid(
'/metadata/lifecycle',
'Battle-tested requires separately governed operational evidence.',
)
}
const draft = await dependencies.store.replaceDraft({
workspaceId: request.workspaceId,
versionId: request.versionId,
updatedBy,
expectedRevision: expected.revision,
expectedDigest: expected.digest,
package: packageValue,
now: dependencies.now(),
})
if (!draft) notFound()
if (draft.publishedAt !== null) {
throw new DomainError(
'private_playbook_published_immutable',
'Published playbook versions cannot be edited',
)
}
return {
draft,
etag: formatPrivatePlaybookDraftEtag(
draft.draftRevision,
draft.draftDigest,
),
}
}
@@ -0,0 +1,251 @@
import { createHash } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import type { WorkspaceAuthorizationLookup } from '../auth/workspace-authorization'
import { createQualityMatrix } from '../quality/static-quality-evaluation'
import type {
PrivatePlaybookDraft,
ValidatedPrivatePlaybookFile,
} from './private-playbook-drafts'
import {
createNextPrivatePlaybookVersion,
publishPrivatePlaybookVersion,
type PrivatePlaybookPublicationCandidate,
type PrivatePlaybookPublicationDependencies,
} from './private-playbook-publication'
const workspaceId = '00000000-0000-4000-8000-000000000001'
const userId = '00000000-0000-4000-8000-000000000002'
const versionId = '00000000-0000-4000-8000-000000000003'
const digest = 'a'.repeat(64)
const actor = { userId }
function file(
path: string,
role: ValidatedPrivatePlaybookFile['role'],
text: string,
): ValidatedPrivatePlaybookFile {
const content = new TextEncoder().encode(text)
return {
path,
role,
mediaType: 'text/markdown',
content,
sizeBytes: content.byteLength,
sha256: createHash('sha256').update(content).digest('hex'),
digest: true,
exportByDefault: true,
}
}
function draft(published = false): PrivatePlaybookDraft {
return {
playbookId: '00000000-0000-4000-8000-000000000004',
versionId,
logicalId: 'private-example',
slug: 'private-example',
semanticVersion: '1.0.0',
title: 'Private example',
lifecycle: 'reviewed',
draftRevision: 3,
draftDigest: digest,
publishedAt: published ? '2026-07-27T12:00:00.000Z' : null,
updatedAt: '2026-07-27T12:00:00.000Z',
packageApiVersion: 'devrunbook.io/v1alpha1',
summary: 'Summary',
category: 'Authoring',
riskTier: 'low',
packageJson: {},
templateText: '# Mission\n',
files: [
file('playbook.yaml', 'manifest', 'kind: PlaybookPackage\n'),
file('CHANGELOG.md', 'changelog', '# Changes\n'),
],
}
}
function candidate(
overrides: Partial<PrivatePlaybookPublicationCandidate['evidence']> = {},
published = false,
): PrivatePlaybookPublicationCandidate {
return {
draft: draft(published),
policy: {
requiredEvaluationCaseIds: ['safe-render'],
minimumRealWorldRuns: 10,
maximumFailureRate: 0.05,
maximumEvidenceAgeDays: 90,
},
evidence: {
schemaAndSemanticValidationPassed: true,
blockingLintFindingCount: 0,
humanEditorialReviewCompleted: true,
limitationsDocumented: true,
evaluationResults: [],
currentEvaluationContext: {
target: { id: versionId, version: '1.0.0', digest },
fixture: {
id: 'fixture',
version: '1.0.0',
digest: 'b'.repeat(64),
environmentDigest: 'c'.repeat(64),
},
},
unresolvedSafetyRegression: false,
realWorldRunCount: 0,
realWorldFailureCount: 0,
unaddressedSevereIncidentCount: 0,
...overrides,
},
}
}
function dependencies(
initialCandidate = candidate(),
): PrivatePlaybookPublicationDependencies {
let current = initialCandidate
const authorization: WorkspaceAuthorizationLookup = {
findWorkspaceAuthorization: vi.fn(async () => ({
userId,
instanceRole: 'user' as const,
workspaceId,
workspaceRole: 'editor' as const,
userStatus: 'active' as const,
})),
}
return {
authorization,
now: () => new Date('2026-07-27T12:00:00.000Z'),
store: {
findPublicationCandidate: vi.fn(async () => current),
publishDraft: vi.fn(async (request) => {
current = {
...current,
draft: {
...current.draft,
lifecycle: request.lifecycle,
publishedAt: request.now.toISOString(),
},
}
return current.draft
}),
createNextDraft: vi.fn(async (request) => ({
...current.draft,
versionId: '00000000-0000-4000-8000-000000000005',
semanticVersion: request.semanticVersion,
lifecycle: 'draft' as const,
draftRevision: 1,
publishedAt: null,
})),
},
}
}
const etag = `"playbook-draft:3:${digest}"`
describe('private playbook publication', () => {
it('publishes a reviewed draft with persisted review evidence', async () => {
const result = await publishPrivatePlaybookVersion(dependencies(), {
actor,
workspaceId,
versionId,
expectedEtag: etag,
lifecycle: 'reviewed',
})
expect(result.version.publishedAt).toBe('2026-07-27T12:00:00.000Z')
expect(result.version.lifecycle).toBe('reviewed')
})
it('rejects stale or missing evaluation evidence for validated', async () => {
const current = candidate().evidence.currentEvaluationContext
const failedOrStale = {
caseId: 'safe-render',
caseVersion: '1.0.0',
target: { ...current.target, digest: 'd'.repeat(64) },
fixture: current.fixture,
status: 'passed' as const,
checks: [],
evaluatedAt: '2026-07-27T11:00:00.000Z',
renderedPromptDigest: 'e'.repeat(64),
dimensions: createQualityMatrix([]),
}
await expect(
publishPrivatePlaybookVersion(
dependencies(candidate({ evaluationResults: [failedOrStale] })),
{
actor,
workspaceId,
versionId,
expectedEtag: etag,
lifecycle: 'validated',
},
),
).rejects.toMatchObject({
code: 'private_playbook_quality_evidence_insufficient',
})
})
it('rejects stale draft review and a missing changelog', async () => {
await expect(
publishPrivatePlaybookVersion(dependencies(), {
actor,
workspaceId,
versionId,
expectedEtag: `"playbook-draft:2:${digest}"`,
lifecycle: 'reviewed',
}),
).rejects.toMatchObject({ code: 'private_playbook_publish_conflict' })
const withoutChangelog = candidate()
const immutableCandidate = {
...withoutChangelog,
draft: {
...withoutChangelog.draft,
files: withoutChangelog.draft.files.slice(0, 1),
},
}
await expect(
publishPrivatePlaybookVersion(dependencies(immutableCandidate), {
actor,
workspaceId,
versionId,
expectedEtag: etag,
lifecycle: 'reviewed',
}),
).rejects.toMatchObject({ code: 'private_playbook_changelog_required' })
})
it('creates a new mutable version from published content', async () => {
const result = await createNextPrivatePlaybookVersion(
dependencies(candidate({}, true)),
{
actor,
workspaceId,
sourceVersionId: versionId,
semanticVersion: '1.1.0',
},
)
expect(result.draft.semanticVersion).toBe('1.1.0')
expect(result.draft.lifecycle).toBe('draft')
expect(result.draft.publishedAt).toBeNull()
})
it('never forks an unpublished source or overwrites its version', async () => {
await expect(
createNextPrivatePlaybookVersion(dependencies(), {
actor,
workspaceId,
sourceVersionId: versionId,
semanticVersion: '1.1.0',
}),
).rejects.toMatchObject({ code: 'private_playbook_publish_conflict' })
await expect(
createNextPrivatePlaybookVersion(dependencies(candidate({}, true)), {
actor,
workspaceId,
sourceVersionId: versionId,
semanticVersion: '1.0.0',
}),
).rejects.toMatchObject({ code: 'private_playbook_publish_conflict' })
})
})
@@ -0,0 +1,212 @@
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import {
assessLifecycleEvidence,
type LifecycleEvidence,
type LifecycleEvidencePolicy,
type PlaybookLifecycle,
} from '../quality/static-quality-evaluation'
import {
formatPrivatePlaybookDraftEtag,
parsePrivatePlaybookDraftEtag,
type PrivatePlaybookDraft,
} from './private-playbook-drafts'
const semverPattern =
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u
export interface PrivatePlaybookPublicationCandidate {
readonly draft: PrivatePlaybookDraft
/** Trusted evidence assembled from persisted validation and evaluation rows. */
readonly evidence: LifecycleEvidence
readonly policy: LifecycleEvidencePolicy
}
export interface PrivatePlaybookPublicationStore {
findPublicationCandidate(
workspaceId: string,
versionId: string,
): Promise<PrivatePlaybookPublicationCandidate | null>
publishDraft(request: {
readonly workspaceId: string
readonly versionId: string
readonly publishedBy: string
readonly expectedRevision: number
readonly expectedDigest: string
readonly lifecycle: Exclude<PlaybookLifecycle, 'draft' | 'battle-tested'>
readonly now: Date
}): Promise<PrivatePlaybookDraft | null>
createNextDraft(request: {
readonly workspaceId: string
readonly sourceVersionId: string
readonly semanticVersion: string
readonly createdBy: string
readonly now: Date
}): Promise<PrivatePlaybookDraft | null>
}
export interface PrivatePlaybookPublicationDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly store: PrivatePlaybookPublicationStore
readonly now: () => Date
}
export interface PrivatePlaybookPublicationRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly versionId: string
readonly expectedEtag: string
readonly lifecycle: Exclude<PlaybookLifecycle, 'draft' | 'battle-tested'>
}
function conflict(message: string, details?: Record<string, unknown>): never {
throw new DomainError('private_playbook_publish_conflict', message, details)
}
function notFound(): never {
throw new DomainError(
'private_playbook_not_found',
'Private playbook not found',
)
}
async function authorizeWrite(
dependencies: PrivatePlaybookPublicationDependencies,
actor: AuthenticatedActor | null,
workspaceId: string,
): Promise<string> {
const context = await authorizeWorkspaceAction(dependencies.authorization, {
actor,
workspaceId,
action: 'write',
})
return context.userId
}
function hasChangelog(draft: PrivatePlaybookDraft): boolean {
return draft.files.some(
(file) => file.role === 'changelog' && file.content.byteLength > 0,
)
}
export async function publishPrivatePlaybookVersion(
dependencies: PrivatePlaybookPublicationDependencies,
request: PrivatePlaybookPublicationRequest,
): Promise<{ readonly version: PrivatePlaybookDraft; readonly etag: string }> {
const publishedBy = await authorizeWrite(
dependencies,
request.actor,
request.workspaceId,
)
const expected = parsePrivatePlaybookDraftEtag(request.expectedEtag)
const candidate = await dependencies.store.findPublicationCandidate(
request.workspaceId,
request.versionId,
)
if (!candidate) notFound()
if (candidate.draft.publishedAt !== null) {
conflict('Published playbook versions are immutable; create a new version.')
}
if (
candidate.draft.draftRevision !== expected.revision ||
candidate.draft.draftDigest !== expected.digest
) {
conflict('The draft changed since it was reviewed.', {
currentEtag: formatPrivatePlaybookDraftEtag(
candidate.draft.draftRevision,
candidate.draft.draftDigest,
),
})
}
if (!hasChangelog(candidate.draft)) {
throw new DomainError(
'private_playbook_changelog_required',
'A non-empty changelog is required before publication',
)
}
const assessment = assessLifecycleEvidence(
request.lifecycle,
candidate.evidence,
candidate.policy,
dependencies.now(),
)
if (!assessment.eligible) {
throw new DomainError(
'private_playbook_quality_evidence_insufficient',
'The requested lifecycle exceeds current quality evidence',
{ requirements: assessment.requirements, findings: assessment.findings },
)
}
const version = await dependencies.store.publishDraft({
workspaceId: request.workspaceId,
versionId: request.versionId,
publishedBy,
expectedRevision: expected.revision,
expectedDigest: expected.digest,
lifecycle: request.lifecycle,
now: dependencies.now(),
})
if (!version) notFound()
return {
version,
etag: formatPrivatePlaybookDraftEtag(
version.draftRevision,
version.draftDigest,
),
}
}
export async function createNextPrivatePlaybookVersion(
dependencies: PrivatePlaybookPublicationDependencies,
request: {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly sourceVersionId: string
readonly semanticVersion: string
},
): Promise<{ readonly draft: PrivatePlaybookDraft; readonly etag: string }> {
const createdBy = await authorizeWrite(
dependencies,
request.actor,
request.workspaceId,
)
if (!semverPattern.test(request.semanticVersion)) {
throw new DomainError(
'private_playbook_version_invalid',
'A valid Semantic Version is required',
)
}
const source = await dependencies.store.findPublicationCandidate(
request.workspaceId,
request.sourceVersionId,
)
if (!source) notFound()
if (source.draft.publishedAt === null) {
conflict('Only an immutable published version can be used as the source.')
}
if (source.draft.semanticVersion === request.semanticVersion) {
conflict('The new version must use a different Semantic Version.')
}
const draft = await dependencies.store.createNextDraft({
workspaceId: request.workspaceId,
sourceVersionId: request.sourceVersionId,
semanticVersion: request.semanticVersion,
createdBy,
now: dependencies.now(),
})
if (!draft) notFound()
return {
draft,
etag: formatPrivatePlaybookDraftEtag(
draft.draftRevision,
draft.draftDigest,
),
}
}
@@ -0,0 +1,165 @@
import { describe, expect, it, vi } from 'vitest'
import { createQualityMatrix } from '../quality/static-quality-evaluation'
import type { PrivatePlaybookDraft } from './private-playbook-drafts'
import {
evaluatePrivatePlaybookStaticCase,
reviewPrivatePlaybookDraft,
type PrivatePlaybookQualityDependencies,
} from './private-playbook-quality'
const userId = '00000000-0000-4000-8000-000000000001'
const workspaceId = '00000000-0000-4000-8000-000000000002'
const versionId = '00000000-0000-4000-8000-000000000003'
const draftDigest = 'a'.repeat(64)
const etag = `"playbook-draft:1:${draftDigest}"`
const actor = { userId }
function draft(): PrivatePlaybookDraft {
return {
playbookId: '00000000-0000-4000-8000-000000000004',
versionId,
logicalId: 'private-example',
slug: 'private-example',
semanticVersion: '1.0.0',
title: 'Private example',
lifecycle: 'draft',
draftRevision: 1,
draftDigest,
publishedAt: null,
updatedAt: '2026-07-27T12:00:00.000Z',
packageApiVersion: 'devrunbook.io/v1alpha1',
summary: 'Summary',
category: 'Authoring',
riskTier: 'low',
packageJson: {
metadata: { lifecycle: 'draft' },
package: { files: [] },
spec: {
intent: {
problem: 'Problem',
outcome: 'Outcome',
whenToUse: ['Now'],
whenNotToUse: ['Never'],
},
inputs: [],
autonomy: { min: 'observe', max: 'repair', default: 'verify' },
workflow: [
{ id: 'inspect', title: 'Inspect', instruction: 'Inspect.' },
],
completion: { criteria: ['Evidence exists.'] },
reporting: { sections: [{ title: 'Evidence' }] },
},
quality: {},
},
templateText: '# Mission\n',
files: [],
}
}
function dependencies(): PrivatePlaybookQualityDependencies {
return {
authorization: {
findWorkspaceAuthorization: vi.fn(async () => ({
userId,
instanceRole: 'user' as const,
workspaceId,
workspaceRole: 'editor' as const,
userStatus: 'active' as const,
})),
},
drafts: {
listDraftsForWorkspace: vi.fn(async () => []),
findVersionForWorkspace: vi.fn(async () => draft()),
createDraft: vi.fn(),
replaceDraft: vi.fn(),
},
quality: {
attestReview: vi.fn(async () => true),
upsertStaticCase: vi.fn(async () => 'case-row'),
appendStaticResult: vi.fn(async () => 'result-row'),
},
now: () => new Date('2026-07-27T12:00:00.000Z'),
}
}
describe('private playbook quality use cases', () => {
it('records human review bound to the exact current digest and computed lint', async () => {
const deps = dependencies()
const result = await reviewPrivatePlaybookDraft(deps, {
actor,
workspaceId,
versionId,
expectedEtag: etag,
limitationsDocumented: true,
unresolvedSafetyRegression: false,
note: 'Reviewed scope, safety, validation and limitations.',
})
expect(result.recorded).toBe(true)
expect(deps.quality.attestReview).toHaveBeenCalledWith(
expect.objectContaining({
attestedDigest: draftDigest,
reviewedBy: userId,
limitationsDocumented: true,
}),
)
})
it('computes and appends literal static evaluation results', async () => {
const deps = dependencies()
const target = {
id: 'private-example',
version: '1.0.0',
digest: draftDigest,
}
const fixture = {
id: 'fixture',
version: '1.0.0',
digest: 'b'.repeat(64),
environmentDigest: 'c'.repeat(64),
}
const result = await evaluatePrivatePlaybookStaticCase(deps, {
actor,
workspaceId,
versionId,
expectedEtag: etag,
evaluationCase: {
id: 'safe-render',
version: '1.0.0',
target,
fixture,
expectedHeadings: ['Mission'],
requiredText: [],
prohibitedText: ['secret-value'],
deterministic: true,
},
observation: {
target,
fixture,
renderedPrompt: '# Mission\nSafe output.\n',
renderedPromptDigest: 'd'.repeat(64),
repeatedRenderDigest: 'd'.repeat(64),
lintStatus: 'ready',
evaluatedAt: '2026-07-27T12:00:00.000Z',
},
dimensions: createQualityMatrix([]),
environment: { composer: 'production' },
})
expect(result.result.status).toBe('passed')
expect(result.resultId).toBe('result-row')
})
it('rejects stale ETags before recording evidence', async () => {
await expect(
reviewPrivatePlaybookDraft(dependencies(), {
actor,
workspaceId,
versionId,
expectedEtag: `"playbook-draft:2:${draftDigest}"`,
limitationsDocumented: true,
unresolvedSafetyRegression: false,
note: 'Reviewed.',
}),
).rejects.toMatchObject({ code: 'private_playbook_quality_conflict' })
})
})
@@ -0,0 +1,236 @@
import { createHash } from 'node:crypto'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
import { lintPlaybookPackage } from '../quality/playbook-package-linter'
import {
evaluateStaticCase,
type QualityMatrix,
type StaticEvaluationCase,
type StaticEvaluationObservation,
type StaticEvaluationResult,
} from '../quality/static-quality-evaluation'
import {
parsePrivatePlaybookDraftEtag,
type PrivatePlaybookDraftStore,
} from './private-playbook-drafts'
export interface PrivatePlaybookQualityStore {
attestReview(attestation: {
readonly workspaceId: string
readonly versionId: string
readonly reviewedBy: string
readonly attestedDigest: string
readonly schemaAndSemanticValidationPassed: boolean
readonly blockingLintFindingCount: number
readonly limitationsDocumented: boolean
readonly unresolvedSafetyRegression: boolean
readonly review: Readonly<Record<string, unknown>>
readonly reviewedAt: Date
}): Promise<boolean>
upsertStaticCase(request: {
readonly workspaceId: string
readonly versionId: string
readonly evaluationCase: StaticEvaluationCase
readonly caseDigest: string
readonly now: Date
}): Promise<string | null>
appendStaticResult(request: {
readonly workspaceId: string
readonly versionId: string
readonly logicalCaseId: string
readonly fixtureVersion: string
readonly result: StaticEvaluationResult
readonly environment: Readonly<Record<string, unknown>>
readonly executedBy: string
readonly now: Date
}): Promise<string | null>
}
export interface PrivatePlaybookQualityDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly drafts: PrivatePlaybookDraftStore
readonly quality: PrivatePlaybookQualityStore
readonly now: () => Date
}
interface QualityActorRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
readonly versionId: string
readonly expectedEtag: string
}
async function authorizeEditor(
dependencies: PrivatePlaybookQualityDependencies,
request: QualityActorRequest,
): Promise<string> {
const context = await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action: 'write',
})
return context.userId
}
async function currentDraft(
dependencies: PrivatePlaybookQualityDependencies,
request: QualityActorRequest,
) {
const draft = await dependencies.drafts.findVersionForWorkspace(
request.workspaceId,
request.versionId,
)
if (!draft)
throw new DomainError(
'private_playbook_not_found',
'Private playbook not found',
)
const expected = parsePrivatePlaybookDraftEtag(request.expectedEtag)
if (
expected.revision !== draft.draftRevision ||
expected.digest !== draft.draftDigest
) {
throw new DomainError(
'private_playbook_quality_conflict',
'The private playbook changed before evidence was recorded',
)
}
if (draft.publishedAt !== null) {
throw new DomainError(
'private_playbook_published_immutable',
'Published playbook versions cannot receive mutable draft evidence',
)
}
return draft
}
function canonical(value: unknown): unknown {
if (Array.isArray(value)) return value.map(canonical)
if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right, 'en'))
.map(([key, child]) => [key, canonical(child)]),
)
}
return value
}
function digest(value: unknown): string {
return createHash('sha256')
.update(JSON.stringify(canonical(value)), 'utf8')
.digest('hex')
}
function reviewNote(value: string): string {
const note = value.trim()
if (note.length < 1 || note.length > 4000) {
throw new DomainError(
'private_playbook_review_invalid',
'Review note must contain between 1 and 4000 characters',
)
}
return note
}
export async function reviewPrivatePlaybookDraft(
dependencies: PrivatePlaybookQualityDependencies,
request: QualityActorRequest & {
readonly limitationsDocumented: boolean
readonly unresolvedSafetyRegression: boolean
readonly note: string
},
) {
const reviewedBy = await authorizeEditor(dependencies, request)
const draft = await currentDraft(dependencies, request)
const lint = lintPlaybookPackage({
packageJson: draft.packageJson,
packageDigest: draft.draftDigest,
template: { path: 'template', content: draft.templateText },
published: false,
})
const blockingLintFindingCount = lint.findings.filter(
(finding) => finding.severity === 'error',
).length
const recorded = await dependencies.quality.attestReview({
workspaceId: request.workspaceId,
versionId: request.versionId,
reviewedBy,
attestedDigest: draft.draftDigest,
schemaAndSemanticValidationPassed: true,
blockingLintFindingCount,
limitationsDocumented: request.limitationsDocumented,
unresolvedSafetyRegression: request.unresolvedSafetyRegression,
review: { note: reviewNote(request.note), lint },
reviewedAt: dependencies.now(),
})
if (!recorded)
throw new DomainError(
'private_playbook_quality_conflict',
'The private playbook changed before review evidence was recorded',
)
return { digest: draft.draftDigest, lint, recorded: true as const }
}
export async function evaluatePrivatePlaybookStaticCase(
dependencies: PrivatePlaybookQualityDependencies,
request: QualityActorRequest & {
readonly evaluationCase: StaticEvaluationCase
readonly observation: StaticEvaluationObservation
readonly dimensions: QualityMatrix
readonly environment: Readonly<Record<string, unknown>>
},
) {
const executedBy = await authorizeEditor(dependencies, request)
const draft = await currentDraft(dependencies, request)
const target = request.evaluationCase.target
if (
target.id !== draft.logicalId ||
target.version !== draft.semanticVersion ||
target.digest !== draft.draftDigest
) {
throw new DomainError(
'private_playbook_evaluation_stale',
'Evaluation case target does not match the current draft',
)
}
const caseId = await dependencies.quality.upsertStaticCase({
workspaceId: request.workspaceId,
versionId: request.versionId,
evaluationCase: request.evaluationCase,
caseDigest: digest(request.evaluationCase),
now: dependencies.now(),
})
if (!caseId)
throw new DomainError(
'private_playbook_evaluation_conflict',
'The private playbook changed before the evaluation case was stored',
)
const result = evaluateStaticCase(
request.evaluationCase,
request.observation,
request.dimensions,
)
const resultId = await dependencies.quality.appendStaticResult({
workspaceId: request.workspaceId,
versionId: request.versionId,
logicalCaseId: request.evaluationCase.id,
fixtureVersion: request.evaluationCase.fixture.version,
result,
environment: request.environment,
executedBy,
now: dependencies.now(),
})
if (!resultId)
throw new DomainError(
'private_playbook_evaluation_conflict',
'The private playbook changed before the result was stored',
)
return { caseId, resultId, result }
}
@@ -0,0 +1,292 @@
import { describe, expect, it } from 'vitest'
import {
lintPlaybookPackage,
type PlaybookPackageLintInput,
} from './playbook-package-linter'
interface MutableManifest {
metadata: { lifecycle: string }
package: { files: unknown[] }
spec: {
intent: unknown
modes: unknown[]
autonomy: unknown
inputs: unknown[]
workflow: unknown[]
validation: unknown
completion: unknown
reporting: unknown
}
quality: { evaluationCaseIds: string[] }
}
function mutableManifest(input: PlaybookPackageLintInput): MutableManifest {
return input.packageJson as unknown as MutableManifest
}
function validInput(
overrides: Partial<PlaybookPackageLintInput> = {},
): PlaybookPackageLintInput {
return {
packageDigest: 'a'.repeat(64),
packageJson: {
metadata: { lifecycle: 'reviewed' },
package: {
files: [{ path: 'CHANGELOG.md', role: 'changelog' }],
},
spec: {
intent: {
problem: 'A bounded problem.',
outcome: 'A verifiable outcome.',
whenToUse: ['For a bounded change.'],
whenNotToUse: ['When authority is missing.'],
},
modes: ['implement'],
autonomy: { min: 'observe', max: 'verify', default: 'implement' },
inputs: [{ key: 'target' }],
workflow: [
{
id: 'implement',
instruction: 'Implement the requested bounded behavior.',
},
],
validation: {
commandRoles: ['build'],
checks: [
{
id: 'build',
description: 'Run the production build.',
evidence: 'Record command result and exit status.',
},
],
},
completion: { criteria: ['The requested behavior is verified.'] },
reporting: {
sections: [
{
id: 'evidence',
description: 'Report inspected evidence sources.',
},
],
},
},
quality: { evaluationCaseIds: [] },
},
template: {
path: 'prompt.md',
content: 'Work only on {{ inputs.target }}.',
},
representativeRenderedPrompts: [
{
path: 'examples/minimal.rendered.md',
content: 'Work only on packages/application. Record evidence.',
},
],
...overrides,
}
}
describe('playbook package linter', () => {
it('reports a clean validated package as export-ready only with exact evidence', () => {
const input = validInput()
const manifest = mutableManifest(input)
manifest.metadata.lifecycle = 'validated'
manifest.quality.evaluationCaseIds = ['bounded.static']
const result = lintPlaybookPackage({
...input,
lifecycleEvidence: {
targetDigest: 'a'.repeat(64),
passingEvaluationCaseIds: ['bounded.static'],
fixtureVersionRecorded: true,
environmentDigest: 'b'.repeat(64),
unresolvedSafetyRegression: false,
},
})
expect(result).toMatchObject({
exportReadiness: 'ready',
findings: [],
provenance: {
kind: 'static-analysis',
source: 'playbook-package-linter/v1',
artifactDigest: 'a'.repeat(64),
},
})
})
it('finds structural, duplicate, autonomy, template and publication defects', () => {
const input = validInput({ published: true })
const manifest = mutableManifest(input)
manifest.spec.intent = {}
manifest.spec.inputs = [{ key: 'same' }, { key: 'same' }]
manifest.spec.workflow = [{ id: 'same' }, { id: 'same' }]
manifest.spec.autonomy = {
min: 'repair',
max: 'observe',
default: 'invalid',
}
manifest.spec.completion = { criteria: [] }
manifest.spec.reporting = { sections: [] }
manifest.package.files = []
const result = lintPlaybookPackage({
...input,
template: { path: 'prompt.md', content: '{{ inputs.missing }}' },
})
expect(result.exportReadiness).toBe('blocked')
expect(result.findings.map((finding) => finding.ruleId)).toEqual([
'PB001',
'PB002',
'PB003',
'PB004',
'PB005',
'PB005',
'PB006',
'PB007',
'PB008',
])
expect(
result.findings.find((finding) => finding.ruleId === 'PB007')?.path,
).toBe('prompt.md:1')
})
it('blocks validated claims when evidence is absent, stale or incomplete', () => {
const input = validInput()
const manifest = mutableManifest(input)
manifest.metadata.lifecycle = 'validated'
manifest.quality.evaluationCaseIds = ['case-a', 'case-b']
const result = lintPlaybookPackage({
...input,
lifecycleEvidence: {
targetDigest: 'stale',
passingEvaluationCaseIds: ['case-a'],
fixtureVersionRecorded: false,
unresolvedSafetyRegression: true,
},
})
expect(result.findings).toEqual([
expect.objectContaining({
ruleId: 'PB009',
path: '/metadata/lifecycle',
provenance: expect.objectContaining({ artifactDigest: 'a'.repeat(64) }),
}),
])
})
it('flags only literal prompt risks and never exposes a matched secret', () => {
const secret = `ghp_${'x'.repeat(32)}`
const result = lintPlaybookPackage({
...validInput(),
representativeRenderedPrompts: [
{
path: 'rendered/risky.md',
content: [
'Improve everything using best practices.',
'Do not modify the repository. Modify the repository and claim success.',
'Drop table users.',
'Run git push and create a release.',
`Use ${secret}`,
].join('\n'),
},
],
repository: {},
})
expect(result.findings.map((finding) => finding.ruleId)).toEqual([
'PR001',
'PR002',
'PR003',
'PR004',
'SA001',
'SA003',
'SA004',
'SA004',
])
expect(JSON.stringify(result)).not.toContain(secret)
})
it('uses explicit repository, trust-boundary and task context for safety and validation rules', () => {
const input = validInput({
taskKind: 'dependency-change',
repository: {
protectedPaths: ['infra/production'],
changeScopePaths: ['infra'],
},
importedContentPlacements: [
{
sourcePath: 'README.md',
destinationSection: 'Authoritative policy',
},
],
})
const result = lintPlaybookPackage(input)
expect(result.findings.map((finding) => finding.ruleId)).toEqual([
'SA002',
'SA005',
'VA003',
])
expect(result.findings.every((finding) => finding.path.length > 0)).toBe(
true,
)
expect(
result.findings.every((finding) => finding.rationale.length > 0),
).toBe(true)
expect(
result.findings.every((finding) => finding.remediation.length > 0),
).toBe(true)
})
it('covers contextual bugfix, implementation, frontend and inspection validation rules', () => {
const base = validInput()
const manifest = mutableManifest(base)
manifest.spec.validation = { commandRoles: [], checks: [] }
manifest.spec.workflow = [{ id: 'work', instruction: 'Perform work.' }]
manifest.spec.reporting = {
sections: [{ id: 'outcome', description: 'Outcome.' }],
}
manifest.spec.modes = []
expect(
lintPlaybookPackage({ ...base, taskKind: 'bugfix' }).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA001' })]),
)
expect(
lintPlaybookPackage({
...base,
taskKind: 'implementation',
repository: { availableCommandRoles: ['build'] },
}).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA002' })]),
)
expect(
lintPlaybookPackage({ ...base, taskKind: 'frontend-flow' }).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA004' })]),
)
expect(
lintPlaybookPackage({ ...base, taskKind: 'inspection' }).findings,
).toEqual(
expect.arrayContaining([expect.objectContaining({ ruleId: 'VA005' })]),
)
})
it('is byte-for-byte deterministic for inert pattern-like package text', () => {
const input = validInput({
template: {
path: 'prompt.md',
content: '(a+)+$ {{ inputs.target }} ${notExecuted} <%= inert %>',
},
})
expect(JSON.stringify(lintPlaybookPackage(input))).toBe(
JSON.stringify(lintPlaybookPackage(input)),
)
})
})
@@ -0,0 +1,763 @@
import {
createQualityFinding,
type QualityFinding,
type QualityProvenance,
} from './static-quality-evaluation'
export type ExportReadiness = 'ready' | 'warning' | 'blocked'
export interface RepresentativeRenderedPrompt {
readonly path: string
readonly content: string
}
export interface RepositoryLintConstraints {
readonly protectedPaths?: readonly string[]
readonly changeScopePaths?: readonly string[]
readonly availableCommandRoles?: readonly string[]
readonly gitPushAuthorized?: boolean
readonly releaseAuthorized?: boolean
}
export interface ImportedContentPlacement {
readonly sourcePath: string
readonly destinationSection: string
}
export interface CurrentLifecycleEvidence {
readonly targetDigest: string
readonly passingEvaluationCaseIds: readonly string[]
readonly fixtureVersionRecorded: boolean
readonly environmentDigest?: string
readonly unresolvedSafetyRegression: boolean
}
export interface PlaybookPackageLintInput {
readonly packageJson: unknown
readonly packageDigest?: string
readonly template: {
readonly path: string
readonly content: string
}
readonly representativeRenderedPrompts?: readonly RepresentativeRenderedPrompt[]
readonly repository?: RepositoryLintConstraints
readonly importedContentPlacements?: readonly ImportedContentPlacement[]
readonly lifecycleEvidence?: CurrentLifecycleEvidence
readonly published?: boolean
readonly taskKind?:
| 'bugfix'
| 'implementation'
| 'dependency-change'
| 'frontend-flow'
| 'inspection'
| 'other'
}
export interface PlaybookPackageLintResult {
readonly exportReadiness: ExportReadiness
readonly findings: readonly QualityFinding[]
readonly provenance: QualityProvenance
}
type JsonRecord = Record<string, unknown>
const AUTONOMY = [
'observe',
'diagnose',
'plan',
'implement',
'verify',
'repair',
] as const
function record(value: unknown): JsonRecord | undefined {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as JsonRecord)
: undefined
}
function records(value: unknown): readonly JsonRecord[] {
return Array.isArray(value)
? value.map(record).filter((item): item is JsonRecord => item !== undefined)
: []
}
function strings(value: unknown): readonly string[] {
return Array.isArray(value)
? value.filter((item): item is string => typeof item === 'string')
: []
}
function text(value: unknown): string {
return typeof value === 'string' ? value.trim() : ''
}
function lower(value: string): string {
return value.toLocaleLowerCase('en-US')
}
function includesAny(value: string, needles: readonly string[]): boolean {
const normalized = lower(value)
return needles.some((needle) => normalized.includes(needle))
}
function uniqueNonEmptyIds(
items: readonly JsonRecord[],
identityField: 'id' | 'key',
): boolean {
const ids = items.map((item) => text(item[identityField]))
return ids.every(Boolean) && ids.length === new Set(ids).size
}
function addFinding(
findings: QualityFinding[],
provenance: QualityProvenance,
finding: Omit<QualityFinding, 'family' | 'provenance' | 'ruleId'> & {
readonly ruleId: string
},
): void {
findings.push(createQualityFinding({ ...finding, provenance }))
}
function declaredTemplateVariables(manifest: JsonRecord): Set<string> {
const spec = record(manifest.spec) ?? {}
const declared = new Set<string>()
for (const input of records(spec.inputs)) {
const key = text(input.key)
if (key) declared.add(`inputs.${key}`)
}
return declared
}
function templateVariables(content: string): readonly {
readonly value: string
readonly offset: number
}[] {
const found: { value: string; offset: number }[] = []
let cursor = 0
while (cursor < content.length) {
const start = content.indexOf('{{', cursor)
if (start < 0) break
const end = content.indexOf('}}', start + 2)
if (end < 0) break
found.push({ value: content.slice(start + 2, end).trim(), offset: start })
cursor = end + 2
}
return found
}
function lineAt(content: string, offset: number): number {
let line = 1
for (let index = 0; index < offset; index += 1)
if (content[index] === '\n') line += 1
return line
}
function hasSecretLikeValue(content: string): boolean {
const normalized = lower(content)
const markers = [
'authorization: bearer ',
'api_key=',
'api-key=',
'access_token=',
'secret=',
'password=',
'ghp_',
'github_pat_',
'sk-proj-',
]
for (const marker of markers) {
let cursor = normalized.indexOf(marker)
while (cursor >= 0) {
const valueStart = cursor + marker.length
let valueLength = 0
for (let index = valueStart; index < content.length; index += 1) {
const character = content[index]!
if (
character === ' ' ||
character === '\t' ||
character === '\r' ||
character === '\n' ||
character === '"' ||
character === "'"
)
break
valueLength += 1
}
if (valueLength >= 8) return true
cursor = normalized.indexOf(marker, valueStart)
}
}
return false
}
function pathOverlaps(left: string, right: string): boolean {
const normalize = (value: string) =>
value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/$/u, '')
const a = normalize(left)
const b = normalize(right)
return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`)
}
function validationText(spec: JsonRecord): string {
const validation = record(spec.validation) ?? {}
return records(validation.checks)
.flatMap((check) => [
text(check.id),
text(check.description),
text(check.evidence),
])
.join('\n')
}
function workflowText(spec: JsonRecord): string {
return records(spec.workflow)
.flatMap((step) => [
text(step.id),
text(step.title),
text(step.instruction),
])
.join('\n')
}
function lintStructure(
manifest: JsonRecord,
findings: QualityFinding[],
provenance: QualityProvenance,
): void {
const spec = record(manifest.spec) ?? {}
const intent = record(spec.intent) ?? {}
if (!text(intent.problem) || !text(intent.outcome))
addFinding(findings, provenance, {
ruleId: 'PB001',
severity: 'error',
path: '/spec/intent',
message: 'The playbook mission is incomplete.',
rationale:
'Both the problem and intended outcome are needed to bound the mission.',
remediation:
'Define non-empty spec.intent.problem and spec.intent.outcome values.',
})
if (
strings(intent.whenToUse).length === 0 ||
strings(intent.whenNotToUse).length === 0
)
addFinding(findings, provenance, {
ruleId: 'PB002',
severity: 'error',
path: '/spec/intent',
message: 'The playbook lacks explicit use and exclusion scope.',
rationale:
'Scope requires both positive applicability and explicit exclusions.',
remediation: 'Add at least one whenToUse and one whenNotToUse entry.',
})
const completion = record(spec.completion) ?? {}
if (strings(completion.criteria).length === 0)
addFinding(findings, provenance, {
ruleId: 'PB003',
severity: 'error',
path: '/spec/completion/criteria',
message: 'The playbook has no done-when criteria.',
rationale: 'Completion cannot be verified without explicit criteria.',
remediation: 'Add concrete, observable completion criteria.',
})
const reporting = record(spec.reporting) ?? {}
if (records(reporting.sections).length === 0)
addFinding(findings, provenance, {
ruleId: 'PB004',
severity: 'error',
path: '/spec/reporting/sections',
message: 'The playbook has no reporting contract.',
rationale: 'A final report must make outcome and evidence reviewable.',
remediation:
'Declare required reporting sections for outcome, evidence, validation and risks.',
})
for (const [path, items, identityField] of [
['/spec/inputs', records(spec.inputs), 'key'],
['/spec/workflow', records(spec.workflow), 'id'],
] as const) {
if (!uniqueNonEmptyIds(items, identityField))
addFinding(findings, provenance, {
ruleId: 'PB005',
severity: 'error',
path,
message: 'Duplicate IDs make the playbook ambiguous.',
rationale:
'Inputs and workflow steps must be addressable by unique IDs.',
remediation: 'Assign a unique ID to every item in this collection.',
})
}
const autonomy = record(spec.autonomy) ?? {}
const minimum = AUTONOMY.indexOf(
text(autonomy.min) as (typeof AUTONOMY)[number],
)
const maximum = AUTONOMY.indexOf(
text(autonomy.max) as (typeof AUTONOMY)[number],
)
const selected = AUTONOMY.indexOf(
text(autonomy.default) as (typeof AUTONOMY)[number],
)
if (
minimum < 0 ||
maximum < 0 ||
selected < minimum ||
selected > maximum ||
minimum > maximum
)
addFinding(findings, provenance, {
ruleId: 'PB006',
severity: 'error',
path: '/spec/autonomy',
message: 'The autonomy range or default is invalid.',
rationale:
'The default must be a known level between the declared minimum and maximum.',
remediation:
'Use Observe through Repair in ascending order and keep the default within the range.',
})
}
function lintTemplate(
input: PlaybookPackageLintInput,
manifest: JsonRecord,
findings: QualityFinding[],
provenance: QualityProvenance,
): void {
const declared = declaredTemplateVariables(manifest)
for (const variable of templateVariables(input.template.content)) {
if (!declared.has(variable.value))
addFinding(findings, provenance, {
ruleId: 'PB007',
severity: 'error',
path: `${input.template.path}:${lineAt(input.template.content, variable.offset)}`,
message: 'The template references an unknown variable.',
rationale: 'Unknown variables cannot be rendered deterministically.',
remediation:
'Declare the input or replace the reference with a declared input variable.',
})
}
}
function lintLifecycle(
input: PlaybookPackageLintInput,
manifest: JsonRecord,
findings: QualityFinding[],
provenance: QualityProvenance,
): void {
const metadata = record(manifest.metadata) ?? {}
const lifecycle = text(metadata.lifecycle)
const packageFiles = records(record(manifest.package)?.files)
if (
input.published === true &&
!packageFiles.some((file) => text(file.role) === 'changelog')
)
addFinding(findings, provenance, {
ruleId: 'PB008',
severity: 'error',
path: '/package/files',
message: 'The published package has no changelog.',
rationale:
'Published versions require a declared changelog for reviewable history.',
remediation:
'Add a changelog file to the package inventory before publication.',
})
if (lifecycle !== 'validated' && lifecycle !== 'battle-tested') return
const quality = record(manifest.quality) ?? {}
const requiredCases = strings(quality.evaluationCaseIds)
const evidence = input.lifecycleEvidence
const exactTarget =
evidence !== undefined &&
input.packageDigest !== undefined &&
evidence.targetDigest === input.packageDigest
const hasAllCases = requiredCases.every((caseId) =>
evidence?.passingEvaluationCaseIds.includes(caseId),
)
const environmentRecorded =
evidence?.fixtureVersionRecorded === true &&
(evidence.environmentDigest?.trim().length ?? 0) > 0
if (
evidence === undefined ||
!exactTarget ||
requiredCases.length === 0 ||
!hasAllCases ||
!environmentRecorded ||
evidence.unresolvedSafetyRegression
)
addFinding(findings, provenance, {
ruleId: 'PB009',
severity: 'error',
path: '/metadata/lifecycle',
message:
'The validated lifecycle lacks current, exact evaluation evidence.',
rationale:
'Validated status requires passing cases bound to this package digest, recorded fixture and environment data, and no unresolved safety regression.',
remediation:
'Record current passing evaluation evidence for every required case or select a lower lifecycle.',
})
}
function lintPromptLanguage(
prompts: readonly RepresentativeRenderedPrompt[],
findings: QualityFinding[],
provenance: QualityProvenance,
): void {
for (const prompt of prompts) {
const value = lower(prompt.content)
if (
includesAny(value, [
'improve everything',
'improve the entire',
'refactor everything',
'refactor the entire repository',
])
)
addFinding(findings, provenance, {
ruleId: 'PR001',
severity: 'warning',
path: prompt.path,
message: 'The prompt contains unbounded improvement language.',
rationale:
'Repository-wide improvement language does not define a reviewable change boundary.',
remediation: 'Name the intended subsystem, behavior and exclusions.',
})
const conflicts = [
['do not modify the repository', 'modify the repository'],
['must not modify the repository', 'modify the repository'],
['do not edit files', 'edit files'],
['must not edit files', 'edit files'],
['read-only mode', 'write to the repository'],
] as const
if (
conflicts.some(
([restriction, mutation]) =>
value.includes(restriction) &&
value.replace(restriction, '').includes(mutation),
)
)
addFinding(findings, provenance, {
ruleId: 'PR002',
severity: 'error',
path: prompt.path,
message: 'The prompt mixes read-only and modification instructions.',
rationale:
'Conflicting authority can cause unsafe or unpredictable execution.',
remediation:
'Resolve the conflict or scope each instruction to distinct, explicit paths.',
})
if (
value.includes('best practices') &&
!includesAny(value, [
'accessibility',
'security',
'performance',
'reliability',
'maintainability',
'compatibility',
])
)
addFinding(findings, provenance, {
ruleId: 'PR003',
severity: 'warning',
path: prompt.path,
message:
'The prompt invokes best practices without quality dimensions.',
rationale: 'An undefined standard cannot be evaluated consistently.',
remediation:
'Name the applicable dimensions, constraints or standards.',
})
if (
includesAny(value, [
'claim success',
'declare success',
'report success',
]) &&
!includesAny(value, [
'evidence',
'test result',
'command result',
'exit status',
])
)
addFinding(findings, provenance, {
ruleId: 'PR004',
severity: 'error',
path: prompt.path,
message: 'The prompt requests a success claim without evidence.',
rationale:
'Success claims must be supported by observable validation results.',
remediation:
'Require actual command, test or inspection evidence and honest failure reporting.',
})
if (hasSecretLikeValue(prompt.content))
addFinding(findings, provenance, {
ruleId: 'SA001',
severity: 'error',
path: prompt.path,
message: 'The rendered prompt contains a secret-like value.',
rationale:
'Credentials and token-like values must not be exported in prompt content.',
remediation:
'Remove the value, rotate it if real, and reference an approved secret mechanism instead.',
})
}
}
function lintSafetyContext(
input: PlaybookPackageLintInput,
manifest: JsonRecord,
prompts: readonly RepresentativeRenderedPrompt[],
findings: QualityFinding[],
provenance: QualityProvenance,
): void {
const repository = input.repository
for (const [index, mutablePath] of (
repository?.changeScopePaths ?? []
).entries()) {
if (
(repository?.protectedPaths ?? []).some((protectedPath) =>
pathOverlaps(mutablePath, protectedPath),
)
)
addFinding(findings, provenance, {
ruleId: 'SA002',
severity: 'error',
path: `repository.changeScopePaths[${index}]`,
message: 'The change scope overlaps a protected path.',
rationale:
'Repository policy takes precedence over a requested modification scope.',
remediation:
'Remove the protected path from change scope or obtain an explicit policy change outside this run.',
})
}
const combined = prompts.map((prompt) => prompt.content).join('\n')
const normalized = lower(combined)
if (
includesAny(normalized, [
'drop table',
'drop column',
'delete all rows',
'truncate table',
]) &&
!includesAny(normalized, ['rollback', 'back up', 'backup'])
)
addFinding(findings, provenance, {
ruleId: 'SA003',
severity: 'error',
path: 'representativeRenderedPrompts',
message: 'A destructive migration lacks rollback or backup instructions.',
rationale: 'Destructive data changes require a recovery path.',
remediation:
'Add an explicit tested rollback or backup-and-restore procedure.',
})
if (
includesAny(normalized, ['git push', 'push the branch']) &&
repository?.gitPushAuthorized !== true
)
addFinding(findings, provenance, {
ruleId: 'SA004',
severity: 'error',
path: 'representativeRenderedPrompts',
message: 'The prompt directs a Git push without explicit authorization.',
rationale:
'External repository mutation requires an explicit authority grant.',
remediation:
'Remove the push instruction or record explicit Git push authorization.',
})
if (
includesAny(normalized, [
'publish the release',
'create a release',
'release to production',
]) &&
repository?.releaseAuthorized !== true
)
addFinding(findings, provenance, {
ruleId: 'SA004',
severity: 'error',
path: 'representativeRenderedPrompts',
message: 'The prompt directs a release without explicit authorization.',
rationale:
'Publishing or releasing is an external side effect requiring explicit authority.',
remediation:
'Remove the release instruction or record explicit release authorization.',
})
for (const [index, placement] of (
input.importedContentPlacements ?? []
).entries()) {
if (
includesAny(placement.destinationSection, [
'policy',
'guardrail',
'authority',
'system instruction',
])
)
addFinding(findings, provenance, {
ruleId: 'SA005',
severity: 'error',
path: `importedContentPlacements[${index}]`,
message:
'Imported content is placed in an authoritative policy section.',
rationale:
'Imported repository and community text is untrusted data, not policy.',
remediation: `Move imported content from the authoritative section into a clearly delimited context section; source path: ${placement.sourcePath}.`,
})
}
// Template text is also exportable content and must never carry credentials.
if (hasSecretLikeValue(input.template.content))
addFinding(findings, provenance, {
ruleId: 'SA001',
severity: 'error',
path: input.template.path,
message: 'The template contains a secret-like value.',
rationale:
'Credentials and token-like values must not be stored in package templates.',
remediation:
'Remove the value, rotate it if real, and use a sensitive input that is excluded from output.',
})
void manifest
}
function lintValidation(
input: PlaybookPackageLintInput,
manifest: JsonRecord,
findings: QualityFinding[],
provenance: QualityProvenance,
): void {
const spec = record(manifest.spec) ?? {}
const workflow = lower(workflowText(spec))
const validation = lower(validationText(spec))
const commandRoles = strings(record(spec.validation)?.commandRoles)
const availableRoles = input.repository?.availableCommandRoles ?? []
const all = `${workflow}\n${validation}\n${commandRoles.join('\n')}`
if (
input.taskKind === 'bugfix' &&
(!includesAny(all, ['reproduc', 'failing test']) ||
!includesAny(all, ['regression', 'test']))
)
addFinding(findings, provenance, {
ruleId: 'VA001',
severity: 'error',
path: '/spec/workflow',
message: 'The bugfix workflow lacks reproduction or regression coverage.',
rationale:
'A bugfix needs evidence of the original failure and protection against recurrence.',
remediation: 'Add explicit reproduction and regression-test steps.',
})
if (input.taskKind === 'implementation') {
const usefulAvailable = availableRoles.filter((role) =>
includesAny(role, ['build', 'test', 'typecheck', 'lint']),
)
if (
usefulAvailable.length > 0 &&
!usefulAvailable.some((role) => commandRoles.includes(role))
)
addFinding(findings, provenance, {
ruleId: 'VA002',
severity: 'error',
path: '/spec/validation/commandRoles',
message: 'Implementation omits available build or test validation.',
rationale:
'Known repository validation should be requested instead of silently skipped.',
remediation:
'Add at least one available build, test, typecheck or lint command role.',
})
}
if (
input.taskKind === 'dependency-change' &&
(!includesAny(all, ['lockfile', 'lock file']) ||
!includesAny(all, ['install']) ||
!includesAny(all, ['build']))
)
addFinding(findings, provenance, {
ruleId: 'VA003',
severity: 'error',
path: '/spec/validation',
message: 'Dependency-change validation is incomplete.',
rationale:
'Dependency changes require lockfile, install and build evidence.',
remediation:
'Require lockfile review plus clean install and production build checks.',
})
if (
input.taskKind === 'frontend-flow' &&
!includesAny(all, ['browser', 'end-to-end', 'e2e', 'keyboard'])
)
addFinding(findings, provenance, {
ruleId: 'VA004',
severity: 'error',
path: '/spec/validation',
message: 'The frontend flow lacks browser verification.',
rationale: 'User-facing behavior cannot be proven by compilation alone.',
remediation: 'Add focused browser verification for the changed flow.',
})
const modes = strings(spec.modes)
if (
(input.taskKind === 'inspection' || modes.includes('inspect')) &&
!includesAny(`${validation}\n${JSON.stringify(spec.reporting ?? '')}`, [
'evidence',
'source',
'inspected',
])
)
addFinding(findings, provenance, {
ruleId: 'VA005',
severity: 'warning',
path: '/spec/reporting',
message:
'The inspection playbook does not require evidence-source reporting.',
rationale:
'Inspection conclusions must identify what was actually examined.',
remediation:
'Require inspected files, commands or other evidence sources in the final report.',
})
}
/**
* Performs deterministic, side-effect-free linting. Package and prompt text is
* inspected only as inert strings; it is never executed or used as a regex or
* template program.
*/
export function lintPlaybookPackage(
input: PlaybookPackageLintInput,
): PlaybookPackageLintResult {
const provenance: QualityProvenance = {
kind: 'static-analysis',
source: 'playbook-package-linter/v1',
...(input.packageDigest === undefined
? {}
: { artifactDigest: input.packageDigest }),
}
const findings: QualityFinding[] = []
const manifest = record(input.packageJson) ?? {}
const prompts = input.representativeRenderedPrompts ?? []
lintStructure(manifest, findings, provenance)
lintTemplate(input, manifest, findings, provenance)
lintLifecycle(input, manifest, findings, provenance)
lintPromptLanguage(prompts, findings, provenance)
lintSafetyContext(input, manifest, prompts, findings, provenance)
lintValidation(input, manifest, findings, provenance)
findings.sort(
(left, right) =>
left.ruleId.localeCompare(right.ruleId) ||
left.path.localeCompare(right.path) ||
left.message.localeCompare(right.message),
)
const exportReadiness: ExportReadiness = findings.some(
(finding) => finding.severity === 'error',
)
? 'blocked'
: findings.some((finding) => finding.severity === 'warning')
? 'warning'
: 'ready'
return { exportReadiness, findings, provenance }
}
@@ -0,0 +1,316 @@
import { describe, expect, it } from 'vitest'
import {
assessLifecycleEvidence,
compareVersionEvaluations,
createQualityFinding,
createQualityMatrix,
evaluateStaticCase,
evaluationFreshness,
QUALITY_DIMENSIONS,
type LifecycleEvidence,
type StaticEvaluationCase,
type StaticEvaluationObservation,
type StaticEvaluationResult,
} from './static-quality-evaluation'
const target = { id: 'playbook', version: '1.0.0', digest: 'a'.repeat(64) }
const fixture = {
id: 'fixture',
version: '2.0.0',
digest: 'b'.repeat(64),
environmentDigest: 'c'.repeat(64),
}
const evaluationCase: StaticEvaluationCase = {
id: 'safe-change.static',
version: '1.0.0',
target,
fixture,
expectedHeadings: ['# Mission'],
requiredText: ['Do not modify protected paths.'],
prohibitedText: ['Authorization: Bearer'],
deterministic: true,
expectedLintStatus: 'ready',
}
const observation: StaticEvaluationObservation = {
target,
fixture,
renderedPrompt: '# Mission\n\nDo not modify protected paths.',
renderedPromptDigest: 'd'.repeat(64),
repeatedRenderDigest: 'd'.repeat(64),
lintStatus: 'ready',
evaluatedAt: '2026-07-27T10:00:00.000Z',
}
function result(
overrides: Partial<StaticEvaluationResult> = {},
): StaticEvaluationResult {
return { ...evaluateStaticCase(evaluationCase, observation), ...overrides }
}
function evidence(
overrides: Partial<LifecycleEvidence> = {},
): LifecycleEvidence {
return {
schemaAndSemanticValidationPassed: true,
blockingLintFindingCount: 0,
humanEditorialReviewCompleted: true,
limitationsDocumented: true,
evaluationResults: [result()],
currentEvaluationContext: { target, fixture },
unresolvedSafetyRegression: false,
realWorldRunCount: 20,
realWorldFailureCount: 1,
unaddressedSevereIncidentCount: 0,
latestRealWorldEvidenceAt: '2026-07-20T10:00:00.000Z',
...overrides,
}
}
const policy = {
requiredEvaluationCaseIds: [evaluationCase.id],
minimumRealWorldRuns: 10,
maximumFailureRate: 0.1,
maximumEvidenceAgeDays: 30,
}
describe('quality findings and dimensions', () => {
it('keeps every dimension visible without inventing an aggregate score', () => {
const matrix = createQualityMatrix([
{
dimension: 'safety',
rating: 'strong',
rationale: 'Protected paths are explicit.',
provenance: [{ kind: 'static-evaluation', source: 'case-1' }],
},
])
expect(Object.keys(matrix)).toEqual(QUALITY_DIMENSIONS)
expect(matrix.safety.rating).toBe('strong')
expect(matrix.reporting.rating).toBe('not-assessed')
expect(matrix).not.toHaveProperty('score')
})
it('creates structured PB/PR/SA/VA findings and rejects unknown families', () => {
const finding = createQualityFinding({
ruleId: 'SA002',
severity: 'error',
path: 'spec.scope.paths[0]',
message: 'Protected path is mutable.',
rationale: 'The scope conflicts with repository policy.',
remediation: 'Exclude the protected path.',
provenance: { kind: 'static-analysis', source: 'prompt-linter' },
})
expect(finding.family).toBe('SA')
expect(() => createQualityFinding({ ...finding, ruleId: 'XX001' })).toThrow(
/PB, PR, SA or VA/,
)
})
})
describe('static evaluation', () => {
it('passes literal expectations tied to exact identities', () => {
const assessed = evaluateStaticCase(evaluationCase, observation)
expect(assessed.status).toBe('passed')
expect(assessed.checks).toHaveLength(7)
expect(assessed.target).toEqual(target)
expect(assessed.fixture).toEqual(fixture)
})
it('treats supplied pattern syntax as literal text, never as a regex', () => {
const assessed = evaluateStaticCase(
{
...evaluationCase,
requiredText: ['(a+)+$'],
prohibitedText: ['.*secret.*'],
},
{ ...observation, renderedPrompt: '# Mission\n(a+)+$' },
)
expect(
assessed.checks.find((check) => check.kind === 'required-text')?.passed,
).toBe(true)
expect(
assessed.checks.find((check) => check.kind === 'prohibited-text')?.passed,
).toBe(true)
})
it('fails changed playbook identity and a mismatched repeated digest', () => {
const assessed = evaluateStaticCase(evaluationCase, {
...observation,
target: { ...target, version: '1.0.1' },
repeatedRenderDigest: 'e'.repeat(64),
})
expect(assessed.status).toBe('failed')
expect(
assessed.checks
.filter((check) => !check.passed)
.map((check) => check.kind),
).toEqual(['identity', 'determinism'])
})
it('detects stale playbook, fixture and environment evidence independently', () => {
expect(
evaluationFreshness(result(), {
target: { ...target, digest: 'x'.repeat(64) },
fixture: {
...fixture,
version: '2.1.0',
environmentDigest: 'y'.repeat(64),
},
}),
).toEqual({
stale: true,
reasons: [
'playbook-digest-changed',
'fixture-version-changed',
'environment-changed',
],
})
})
})
describe('lifecycle evidence policy', () => {
it('allows draft without presenting it as evidence-backed', () => {
expect(
assessLifecycleEvidence(
'draft',
evidence({ evaluationResults: [] }),
policy,
new Date(),
),
).toMatchObject({ eligible: true, requirements: [], findings: [] })
})
it('blocks reviewed when editorial requirements are missing', () => {
const assessed = assessLifecycleEvidence(
'reviewed',
evidence({ humanEditorialReviewCompleted: false }),
policy,
new Date('2026-07-27T10:00:00.000Z'),
)
expect(assessed.eligible).toBe(false)
expect(assessed.findings[0]).toMatchObject({
ruleId: 'PB009',
family: 'PB',
})
})
it('blocks validated when required evidence is stale', () => {
const assessed = assessLifecycleEvidence(
'validated',
evidence({
currentEvaluationContext: {
target: { ...target, version: '1.1.0' },
fixture,
},
}),
policy,
new Date('2026-07-27T10:00:00.000Z'),
)
expect(assessed.eligible).toBe(false)
expect(
assessed.requirements.find((item) => item.id.startsWith('evaluation:')),
).toMatchObject({
satisfied: false,
})
})
it('requires real-world volume, failure rate, incident and recency evidence for battle-tested', () => {
const accepted = assessLifecycleEvidence(
'battle-tested',
evidence(),
policy,
new Date('2026-07-27T10:00:00.000Z'),
)
expect(accepted.eligible).toBe(true)
const rejected = assessLifecycleEvidence(
'battle-tested',
evidence({
realWorldRunCount: 2,
realWorldFailureCount: 1,
unaddressedSevereIncidentCount: 1,
latestRealWorldEvidenceAt: '2025-01-01T00:00:00.000Z',
}),
policy,
new Date('2026-07-27T10:00:00.000Z'),
)
expect(
rejected.requirements
.filter((item) => !item.satisfied)
.map((item) => item.id),
).toEqual([
'real-world-runs',
'failure-rate',
'severe-incidents',
'evidence-recency',
])
})
it('requires a rationale or replacement for deprecated lifecycle', () => {
expect(
assessLifecycleEvidence('deprecated', evidence(), policy, new Date())
.eligible,
).toBe(false)
expect(
assessLifecycleEvidence(
'deprecated',
evidence({ replacementPlaybookId: 'safe-change-v2' }),
policy,
new Date(),
).eligible,
).toBe(true)
})
})
describe('version comparison', () => {
it('compares common exact cases and reports dimension changes separately', () => {
const previous = result({
status: 'failed',
dimensions: createQualityMatrix([
{
dimension: 'verification',
rating: 'weak',
rationale: 'Missing evidence.',
provenance: [],
},
]),
})
const current = result({
target: { ...target, version: '1.1.0', digest: 'f'.repeat(64) },
dimensions: createQualityMatrix([
{
dimension: 'verification',
rating: 'strong',
rationale: 'Evidence is explicit.',
provenance: [
{ kind: 'static-evaluation', source: evaluationCase.id },
],
},
]),
})
expect(compareVersionEvaluations([previous], [current])).toEqual({
newlyPassingCaseIds: [evaluationCase.id],
newlyFailingCaseIds: [],
unchangedCaseIds: [],
dimensionChanges: [
{
caseId: evaluationCase.id,
dimension: 'verification',
previous: 'weak',
current: 'strong',
},
],
})
})
it('does not compare results from a changed fixture', () => {
const changedFixture = result({ fixture: { ...fixture, version: '3.0.0' } })
expect(compareVersionEvaluations([result()], [changedFixture])).toEqual({
newlyPassingCaseIds: [],
newlyFailingCaseIds: [],
unchangedCaseIds: [],
dimensionChanges: [],
})
})
})
@@ -0,0 +1,554 @@
export const QUALITY_DIMENSIONS = [
'scope-clarity',
'safety',
'verification',
'reproducibility',
'compatibility',
'reporting',
'efficiency',
] as const
export type QualityDimension = (typeof QUALITY_DIMENSIONS)[number]
export type QualityRating = 'not-assessed' | 'weak' | 'adequate' | 'strong'
export type QualityFindingSeverity = 'info' | 'warning' | 'error'
export type QualityFindingFamily = 'PB' | 'PR' | 'SA' | 'VA'
export type QualityEvidenceKind =
| 'authored-claim'
| 'static-analysis'
| 'static-evaluation'
| 'executed-evaluation'
| 'operator-feedback'
export interface QualityProvenance {
readonly kind: QualityEvidenceKind
readonly source: string
readonly observedAt?: string
readonly artifactDigest?: string
}
export interface QualityFinding {
readonly ruleId: `${QualityFindingFamily}${string}`
readonly family: QualityFindingFamily
readonly severity: QualityFindingSeverity
readonly path: string
readonly message: string
readonly rationale: string
readonly remediation: string
readonly provenance: QualityProvenance
}
export interface QualityDimensionAssessment {
readonly dimension: QualityDimension
readonly rating: QualityRating
readonly rationale: string
readonly provenance: readonly QualityProvenance[]
}
export type QualityMatrix = Readonly<
Record<QualityDimension, QualityDimensionAssessment>
>
export type PlaybookLifecycle =
'draft' | 'reviewed' | 'validated' | 'battle-tested' | 'deprecated'
export interface VersionIdentity {
readonly id: string
readonly version: string
readonly digest: string
}
export interface FixtureIdentity extends VersionIdentity {
readonly environmentDigest: string
}
export interface StaticEvaluationCase {
readonly id: string
readonly version: string
readonly target: VersionIdentity
readonly fixture: FixtureIdentity
readonly expectedHeadings: readonly string[]
readonly requiredText: readonly string[]
readonly prohibitedText: readonly string[]
readonly deterministic: boolean
readonly expectedLintStatus?: 'ready' | 'warning' | 'blocked'
}
export interface StaticEvaluationObservation {
readonly target: VersionIdentity
readonly fixture: FixtureIdentity
readonly renderedPrompt: string
readonly renderedPromptDigest: string
readonly repeatedRenderDigest?: string
readonly lintStatus: 'ready' | 'warning' | 'blocked'
readonly evaluatedAt: string
}
export type StaticEvaluationCheckKind =
| 'identity'
| 'expected-heading'
| 'required-text'
| 'prohibited-text'
| 'determinism'
| 'lint-status'
export interface StaticEvaluationCheck {
readonly kind: StaticEvaluationCheckKind
readonly path: string
readonly passed: boolean
readonly rationale: string
}
export interface StaticEvaluationResult {
readonly caseId: string
readonly caseVersion: string
readonly target: VersionIdentity
readonly fixture: FixtureIdentity
readonly status: 'passed' | 'failed'
readonly checks: readonly StaticEvaluationCheck[]
readonly evaluatedAt: string
readonly renderedPromptDigest: string
readonly dimensions: QualityMatrix
}
export interface CurrentEvaluationContext {
readonly target: VersionIdentity
readonly fixture: FixtureIdentity
}
export interface EvaluationFreshness {
readonly stale: boolean
readonly reasons: readonly (
| 'playbook-id-changed'
| 'playbook-version-changed'
| 'playbook-digest-changed'
| 'fixture-id-changed'
| 'fixture-version-changed'
| 'fixture-digest-changed'
| 'environment-changed'
)[]
}
function familyForRuleId(ruleId: string): QualityFindingFamily | null {
const prefix = ruleId.slice(0, 2)
if (prefix === 'PB' || prefix === 'PR' || prefix === 'SA' || prefix === 'VA')
return prefix
return null
}
function isRuleId(
ruleId: string,
): ruleId is `${QualityFindingFamily}${string}` {
const family = familyForRuleId(ruleId)
if (family === null || ruleId.length !== 5) return false
for (const character of ruleId.slice(2)) {
if (character < '0' || character > '9') return false
}
return true
}
/** Creates a serializable finding while rejecting ambiguous rule identifiers. */
export function createQualityFinding(
finding: Omit<QualityFinding, 'family' | 'ruleId'> & {
readonly ruleId: string
},
): QualityFinding {
if (!isRuleId(finding.ruleId))
throw new Error(
'Quality rule IDs must use PB, PR, SA or VA plus three digits',
)
return {
...finding,
ruleId: finding.ruleId,
family: familyForRuleId(finding.ruleId)!,
}
}
export function createQualityMatrix(
assessments: readonly QualityDimensionAssessment[],
): QualityMatrix {
const byDimension = new Map(
assessments.map((assessment) => [assessment.dimension, assessment]),
)
return Object.fromEntries(
QUALITY_DIMENSIONS.map((dimension) => [
dimension,
byDimension.get(dimension) ?? {
dimension,
rating: 'not-assessed',
rationale: 'No evidence has been recorded for this dimension.',
provenance: [],
},
]),
) as QualityMatrix
}
function sameIdentity(left: VersionIdentity, right: VersionIdentity): boolean {
return (
left.id === right.id &&
left.version === right.version &&
left.digest === right.digest
)
}
function literalIncludes(content: string, expected: string): boolean {
return content.includes(expected)
}
/**
* Evaluates declarative, literal prompt expectations only. Case values are never
* interpreted as regular expressions, templates, JavaScript or shell commands.
*/
export function evaluateStaticCase(
evaluationCase: StaticEvaluationCase,
observation: StaticEvaluationObservation,
dimensions: QualityMatrix = createQualityMatrix([]),
): StaticEvaluationResult {
const checks: StaticEvaluationCheck[] = [
{
kind: 'identity',
path: 'target',
passed: sameIdentity(evaluationCase.target, observation.target),
rationale:
'Observation must match the exact playbook ID, version and digest.',
},
{
kind: 'identity',
path: 'fixture',
passed:
sameIdentity(evaluationCase.fixture, observation.fixture) &&
evaluationCase.fixture.environmentDigest ===
observation.fixture.environmentDigest,
rationale:
'Observation must match the exact fixture ID, version, digest and environment digest.',
},
]
for (const [index, heading] of evaluationCase.expectedHeadings.entries()) {
checks.push({
kind: 'expected-heading',
path: `expectedHeadings[${index}]`,
passed: literalIncludes(observation.renderedPrompt, heading),
rationale: `Rendered prompt must contain the declared heading ${JSON.stringify(heading)}.`,
})
}
for (const [index, required] of evaluationCase.requiredText.entries()) {
checks.push({
kind: 'required-text',
path: `requiredText[${index}]`,
passed: literalIncludes(observation.renderedPrompt, required),
rationale: `Rendered prompt must contain the declared literal text ${JSON.stringify(required)}.`,
})
}
for (const [index, prohibited] of evaluationCase.prohibitedText.entries()) {
checks.push({
kind: 'prohibited-text',
path: `prohibitedText[${index}]`,
passed: !literalIncludes(observation.renderedPrompt, prohibited),
rationale: `Rendered prompt must not contain the declared literal text ${JSON.stringify(prohibited)}.`,
})
}
if (evaluationCase.deterministic) {
checks.push({
kind: 'determinism',
path: 'deterministic',
passed:
observation.repeatedRenderDigest !== undefined &&
observation.renderedPromptDigest === observation.repeatedRenderDigest,
rationale: 'Repeated rendering must produce the same content digest.',
})
}
if (evaluationCase.expectedLintStatus !== undefined) {
checks.push({
kind: 'lint-status',
path: 'expectedLintStatus',
passed: observation.lintStatus === evaluationCase.expectedLintStatus,
rationale: `Lint status must be ${evaluationCase.expectedLintStatus}.`,
})
}
return {
caseId: evaluationCase.id,
caseVersion: evaluationCase.version,
target: observation.target,
fixture: observation.fixture,
status: checks.every((check) => check.passed) ? 'passed' : 'failed',
checks,
evaluatedAt: observation.evaluatedAt,
renderedPromptDigest: observation.renderedPromptDigest,
dimensions,
}
}
export function evaluationFreshness(
result: StaticEvaluationResult,
current: CurrentEvaluationContext,
): EvaluationFreshness {
const reasons: EvaluationFreshness['reasons'][number][] = []
if (result.target.id !== current.target.id)
reasons.push('playbook-id-changed')
if (result.target.version !== current.target.version)
reasons.push('playbook-version-changed')
if (result.target.digest !== current.target.digest)
reasons.push('playbook-digest-changed')
if (result.fixture.id !== current.fixture.id)
reasons.push('fixture-id-changed')
if (result.fixture.version !== current.fixture.version)
reasons.push('fixture-version-changed')
if (result.fixture.digest !== current.fixture.digest)
reasons.push('fixture-digest-changed')
if (result.fixture.environmentDigest !== current.fixture.environmentDigest)
reasons.push('environment-changed')
return { stale: reasons.length > 0, reasons }
}
export interface LifecycleEvidencePolicy {
readonly requiredEvaluationCaseIds: readonly string[]
readonly minimumRealWorldRuns: number
readonly maximumFailureRate: number
readonly maximumEvidenceAgeDays: number
}
export interface LifecycleEvidence {
readonly schemaAndSemanticValidationPassed: boolean
readonly blockingLintFindingCount: number
readonly humanEditorialReviewCompleted: boolean
readonly limitationsDocumented: boolean
readonly evaluationResults: readonly StaticEvaluationResult[]
readonly currentEvaluationContext: CurrentEvaluationContext
readonly unresolvedSafetyRegression: boolean
readonly realWorldRunCount: number
readonly realWorldFailureCount: number
readonly unaddressedSevereIncidentCount: number
readonly latestRealWorldEvidenceAt?: string
readonly deprecationRationale?: string
readonly replacementPlaybookId?: string
}
export interface LifecycleRequirement {
readonly id: string
readonly satisfied: boolean
readonly rationale: string
}
export interface LifecycleAssessment {
readonly requestedLifecycle: PlaybookLifecycle
readonly eligible: boolean
readonly requirements: readonly LifecycleRequirement[]
readonly findings: readonly QualityFinding[]
}
function reviewedRequirements(
evidence: LifecycleEvidence,
): LifecycleRequirement[] {
return [
{
id: 'schema-semantic-validation',
satisfied: evidence.schemaAndSemanticValidationPassed,
rationale: 'Schema and semantic validation must pass.',
},
{
id: 'blocking-lint',
satisfied: evidence.blockingLintFindingCount === 0,
rationale: 'Required examples must have no blocking lint findings.',
},
{
id: 'editorial-review',
satisfied: evidence.humanEditorialReviewCompleted,
rationale: 'A human editorial review must be complete.',
},
{
id: 'limitations',
satisfied: evidence.limitationsDocumented,
rationale: 'Known limitations must be documented.',
},
]
}
function requiredEvaluationRequirement(
evidence: LifecycleEvidence,
caseId: string,
): LifecycleRequirement {
const matching = evidence.evaluationResults.filter(
(result) => result.caseId === caseId,
)
const currentPassed = matching.some(
(result) =>
result.status === 'passed' &&
!evaluationFreshness(result, evidence.currentEvaluationContext).stale,
)
return {
id: `evaluation:${caseId}`,
satisfied: currentPassed,
rationale: currentPassed
? 'Required evaluation passed against the current playbook and fixture.'
: 'Required evaluation lacks current passing evidence.',
}
}
function evidenceIsRecent(
evidenceAt: string | undefined,
now: Date,
maximumAgeDays: number,
): boolean {
if (evidenceAt === undefined) return false
const parsed = new Date(evidenceAt)
if (Number.isNaN(parsed.getTime())) return false
const age = now.getTime() - parsed.getTime()
return age >= 0 && age <= maximumAgeDays * 24 * 60 * 60 * 1000
}
export function assessLifecycleEvidence(
requestedLifecycle: PlaybookLifecycle,
evidence: LifecycleEvidence,
policy: LifecycleEvidencePolicy,
now: Date,
): LifecycleAssessment {
let requirements: LifecycleRequirement[] = []
if (requestedLifecycle === 'reviewed')
requirements = reviewedRequirements(evidence)
if (
requestedLifecycle === 'validated' ||
requestedLifecycle === 'battle-tested'
) {
requirements = [
...reviewedRequirements(evidence),
...policy.requiredEvaluationCaseIds.map((caseId) =>
requiredEvaluationRequirement(evidence, caseId),
),
{
id: 'safety-regression',
satisfied: !evidence.unresolvedSafetyRegression,
rationale: 'No unresolved safety regression may remain.',
},
]
}
if (requestedLifecycle === 'battle-tested') {
const failureRate =
evidence.realWorldRunCount === 0
? Number.POSITIVE_INFINITY
: evidence.realWorldFailureCount / evidence.realWorldRunCount
requirements.push(
{
id: 'real-world-runs',
satisfied: evidence.realWorldRunCount >= policy.minimumRealWorldRuns,
rationale: `At least ${policy.minimumRealWorldRuns} real-world runs are required.`,
},
{
id: 'failure-rate',
satisfied: failureRate <= policy.maximumFailureRate,
rationale: `Failure rate must not exceed ${policy.maximumFailureRate}.`,
},
{
id: 'severe-incidents',
satisfied: evidence.unaddressedSevereIncidentCount === 0,
rationale: 'No severe incident may remain unaddressed.',
},
{
id: 'evidence-recency',
satisfied: evidenceIsRecent(
evidence.latestRealWorldEvidenceAt,
now,
policy.maximumEvidenceAgeDays,
),
rationale: `Real-world evidence must be no older than ${policy.maximumEvidenceAgeDays} days.`,
},
)
}
if (requestedLifecycle === 'deprecated') {
requirements = [
{
id: 'deprecation-rationale',
satisfied:
(evidence.deprecationRationale?.trim().length ?? 0) > 0 ||
(evidence.replacementPlaybookId?.trim().length ?? 0) > 0,
rationale: 'A deprecation rationale or replacement must be provided.',
},
]
}
const eligible = requirements.every((requirement) => requirement.satisfied)
const findings = eligible
? []
: [
createQualityFinding({
ruleId: 'PB009',
severity: 'error',
path: 'metadata.lifecycle',
message: `${requestedLifecycle} lifecycle lacks required evidence.`,
rationale: requirements
.filter((requirement) => !requirement.satisfied)
.map((requirement) => requirement.rationale)
.join(' '),
remediation:
'Supply current evidence for every failed lifecycle requirement or select a lower lifecycle.',
provenance: {
kind: 'static-analysis',
source: 'lifecycle-evidence-policy',
},
}),
]
return { requestedLifecycle, eligible, requirements, findings }
}
export interface VersionEvaluationComparison {
readonly newlyPassingCaseIds: readonly string[]
readonly newlyFailingCaseIds: readonly string[]
readonly unchangedCaseIds: readonly string[]
readonly dimensionChanges: readonly {
readonly caseId: string
readonly dimension: QualityDimension
readonly previous: QualityRating
readonly current: QualityRating
}[]
}
function comparisonKey(result: StaticEvaluationResult): string {
return `${result.caseId}\u0000${result.caseVersion}\u0000${result.fixture.id}\u0000${result.fixture.version}\u0000${result.fixture.digest}\u0000${result.fixture.environmentDigest}`
}
/** Compares only cases with the same case and fixture identity. */
export function compareVersionEvaluations(
previous: readonly StaticEvaluationResult[],
current: readonly StaticEvaluationResult[],
): VersionEvaluationComparison {
const previousByCase = new Map(
previous.map((result) => [comparisonKey(result), result]),
)
const newlyPassingCaseIds: string[] = []
const newlyFailingCaseIds: string[] = []
const unchangedCaseIds: string[] = []
const dimensionChanges: VersionEvaluationComparison['dimensionChanges'][number][] =
[]
for (const currentResult of current) {
const previousResult = previousByCase.get(comparisonKey(currentResult))
if (previousResult === undefined) continue
if (previousResult.status === 'failed' && currentResult.status === 'passed')
newlyPassingCaseIds.push(currentResult.caseId)
else if (
previousResult.status === 'passed' &&
currentResult.status === 'failed'
)
newlyFailingCaseIds.push(currentResult.caseId)
else unchangedCaseIds.push(currentResult.caseId)
for (const dimension of QUALITY_DIMENSIONS) {
const previousRating = previousResult.dimensions[dimension].rating
const currentRating = currentResult.dimensions[dimension].rating
if (previousRating !== currentRating)
dimensionChanges.push({
caseId: currentResult.caseId,
dimension,
previous: previousRating,
current: currentRating,
})
}
}
return {
newlyPassingCaseIds: [...new Set(newlyPassingCaseIds)].sort(),
newlyFailingCaseIds: [...new Set(newlyFailingCaseIds)].sort(),
unchangedCaseIds: [...new Set(unchangedCaseIds)].sort(),
dimensionChanges,
}
}
@@ -0,0 +1,62 @@
import { describe, expect, it, vi } from 'vitest'
import {
listRepositoryPreferences,
setRepositoryPreference,
type RepositoryPreferenceStore,
} from './repository-preferences'
const actor = {
userId: 'user-1',
workspaceId: 'workspace-1',
workspaceRole: 'viewer',
instanceRole: 'user',
} as const
describe('repository preferences', () => {
it('always scopes personal ordering to the authenticated workspace and user', async () => {
const list = vi.fn(async () => [])
const store: RepositoryPreferenceStore = {
list,
set: vi.fn(async () => true),
}
await expect(listRepositoryPreferences(store, actor)).resolves.toEqual([])
expect(list).toHaveBeenCalledWith({
workspaceId: 'workspace-1',
userId: 'user-1',
})
})
it('persists favorite and last-used state without granting repository access', async () => {
const set = vi.fn(async () => true)
const store: RepositoryPreferenceStore = {
list: vi.fn(async () => []),
set,
}
await setRepositoryPreference(store, actor, {
repositoryId: 'repository-1',
favorite: true,
markUsed: true,
})
expect(set).toHaveBeenCalledWith({
workspaceId: 'workspace-1',
userId: 'user-1',
repositoryId: 'repository-1',
favorite: true,
markUsed: true,
})
})
it('maps a cross-workspace or missing repository to the same not-found error', async () => {
const store: RepositoryPreferenceStore = {
list: vi.fn(async () => []),
set: vi.fn(async () => false),
}
await expect(
setRepositoryPreference(store, actor, {
repositoryId: 'repository-outside-scope',
favorite: true,
}),
).rejects.toMatchObject({ code: 'repository_preference_not_found' })
})
})
@@ -0,0 +1,60 @@
import { DomainError } from '@devrunbook/domain'
import type { ActorContext } from '../auth/workspace-authorization'
export interface RepositoryPreference {
readonly repositoryId: string
readonly favorite: boolean
readonly lastUsedAt: Date | null
}
export interface RepositoryPreferenceStore {
list(input: {
readonly workspaceId: string
readonly userId: string
}): Promise<readonly RepositoryPreference[]>
set(input: {
readonly workspaceId: string
readonly userId: string
readonly repositoryId: string
readonly favorite?: boolean
readonly markUsed?: boolean
}): Promise<boolean>
}
export function listRepositoryPreferences(
store: RepositoryPreferenceStore,
actor: ActorContext,
) {
return store.list({ workspaceId: actor.workspaceId, userId: actor.userId })
}
export async function setRepositoryPreference(
store: RepositoryPreferenceStore,
actor: ActorContext,
input: {
readonly repositoryId: string
readonly favorite?: boolean
readonly markUsed?: boolean
},
): Promise<void> {
if (input.favorite === undefined && input.markUsed !== true) {
throw new DomainError(
'repository_preference_invalid',
'No repository preference change was supplied',
)
}
const found = await store.set({
workspaceId: actor.workspaceId,
userId: actor.userId,
repositoryId: input.repositoryId,
...(input.favorite === undefined ? {} : { favorite: input.favorite }),
...(input.markUsed === undefined ? {} : { markUsed: input.markUsed }),
})
if (!found) {
throw new DomainError(
'repository_preference_not_found',
'Repository was not found',
)
}
}
@@ -0,0 +1,471 @@
import {
applyRepositoryProfileServerMetadata,
digestRepositoryProfile,
type RepositoryProfile,
} from '@devrunbook/repository-intel'
import { describe, expect, it, vi } from 'vitest'
import type {
WorkspaceAuthorizationLookup,
WorkspaceAuthorizationRecord,
WorkspaceRole,
} from '../auth/workspace-authorization'
import {
appendRepositoryProfileRevision,
createManualRepository,
exportCurrentRepositoryProfile,
formatStrongProfileEtag,
getCurrentRepositoryProfile,
getRepository,
listRepositories,
parseStrongProfileEtag,
type AppendRepositoryProfileRevisionStoreRequest,
type CreateManualRepositoryStoreRequest,
type RepositoryProfileRevision,
type RepositoryStore,
type RepositorySummary,
} from './repository-profiles'
const userId = '00000000-0000-4000-8000-000000000001'
const workspaceId = '00000000-0000-4000-8000-000000000002'
const repositoryId = '00000000-0000-4000-8000-000000000003'
const digest = 'a'.repeat(64)
function authorizationRecord(
workspaceRole: WorkspaceRole,
overrides: Partial<WorkspaceAuthorizationRecord> = {},
): WorkspaceAuthorizationRecord {
return {
userId,
workspaceId,
workspaceRole,
instanceRole: 'user',
userStatus: 'active',
...overrides,
}
}
class Authorization implements WorkspaceAuthorizationLookup {
readonly findWorkspaceAuthorization = vi.fn(
async (resolvedUserId: string, resolvedWorkspaceId: string) => {
void resolvedUserId
void resolvedWorkspaceId
return this.record
},
)
constructor(readonly record: WorkspaceAuthorizationRecord | null) {}
}
function profile(overrides: Partial<RepositoryProfile['metadata']> = {}) {
return {
apiVersion: 'devrunbook.io/v1alpha1',
kind: 'RepositoryProfile',
metadata: {
name: 'Client supplied name',
revision: 99,
source: 'gitea',
contentDigest: '0'.repeat(64),
...overrides,
},
spec: {
repositoryType: 'single-app',
defaultBranch: 'main',
stack: {
languages: ['TypeScript'],
frameworks: ['Next.js'],
packageManagers: ['pnpm'],
databases: ['PostgreSQL'],
deploymentTypes: ['Docker'],
testFrameworks: ['Vitest'],
},
commands: [
{
id: 'test',
role: 'unit-test',
command: 'pnpm test && printf "$(inert)"',
workingDirectory: '.',
platform: 'any',
shell: 'auto',
source: 'manual',
confirmed: true,
safeForAgentSuggestion: true,
},
],
paths: {
applicationRoots: ['apps/web'],
testRoots: ['tests'],
documentationRoots: ['docs'],
generated: ['dist'],
protected: ['runtime'],
excluded: ['node_modules'],
},
policies: {
preserveBackwardCompatibility: true,
newDependencies: 'justify',
gitWrite: 'none',
migrations: 'reversible-only',
documentationRequired: true,
networkAccess: 'forbidden',
productionDataAccess: 'forbidden',
},
},
} satisfies RepositoryProfile
}
const repository: RepositorySummary = {
id: repositoryId,
displayName: 'Example repository',
sourceType: 'manual',
defaultBranch: 'main',
archived: false,
currentProfileRevision: 1,
lastSnapshotAt: null,
createdAt: '2026-07-27T00:00:00.000Z',
updatedAt: '2026-07-27T00:00:00.000Z',
}
function revision(
document = applyRepositoryProfileServerMetadata(profile(), 1),
) {
return {
id: '00000000-0000-4000-8000-000000000004',
repositoryId,
revisionNumber: document.metadata.revision,
profile: document,
contentDigest: document.metadata.contentDigest!,
createdBy: userId,
createdAt: '2026-07-27T00:00:00.000Z',
} satisfies RepositoryProfileRevision
}
class MemoryStore implements RepositoryStore {
readonly listForWorkspace = vi.fn<RepositoryStore['listForWorkspace']>(
async () => ({ items: [repository], nextCursor: null }),
)
readonly findByIdForWorkspace = vi.fn<
RepositoryStore['findByIdForWorkspace']
>(async () => repository)
readonly findCurrentProfileForWorkspace = vi.fn<
RepositoryStore['findCurrentProfileForWorkspace']
>(async () => revision())
readonly createManualWithInitialProfile = vi.fn<
RepositoryStore['createManualWithInitialProfile']
>(async (request: CreateManualRepositoryStoreRequest) => ({
repository,
revision: revision(request.initialProfile),
}))
readonly appendImmutableRevision = vi.fn<
RepositoryStore['appendImmutableRevision']
>(async (request: AppendRepositoryProfileRevisionStoreRequest) => {
void request
return { revision: revision(), created: false }
})
}
function dependencies(
role: WorkspaceRole = 'owner',
store = new MemoryStore(),
authorization: WorkspaceAuthorizationRecord | null = authorizationRecord(
role,
),
) {
return {
authorization: new Authorization(authorization),
store,
now: () => new Date('2026-07-27T12:34:56.000Z'),
}
}
const actor = { userId }
describe('repository actor boundaries', () => {
it('rejects unauthenticated calls before touching persistence', async () => {
const target = dependencies()
await expect(
listRepositories(target, { actor: null, workspaceId }),
).rejects.toMatchObject({ code: 'authentication_required' })
expect(target.store.listForWorkspace).not.toHaveBeenCalled()
})
it.each([
['no membership', null],
['disabled user', authorizationRecord('owner', { userStatus: 'disabled' })],
['admin without membership', null],
] as const)(
'denies %s with the generic workspace boundary',
async (_name, record) => {
const target = dependencies('owner', new MemoryStore(), record)
await expect(
listRepositories(target, { actor, workspaceId }),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
expect(target.store.listForWorkspace).not.toHaveBeenCalled()
},
)
it('allows viewers to use every read use case and export', async () => {
const target = dependencies('viewer')
await expect(
listRepositories(target, { actor, workspaceId }),
).resolves.toMatchObject({ items: [repository] })
await expect(
getRepository(target, { actor, workspaceId, repositoryId }),
).resolves.toEqual(repository)
await expect(
getCurrentRepositoryProfile(target, {
actor,
workspaceId,
repositoryId,
}),
).resolves.toMatchObject({ revision: { repositoryId } })
await expect(
exportCurrentRepositoryProfile(target, {
actor,
workspaceId,
repositoryId,
format: 'json',
}),
).resolves.toMatchObject({
contentType: 'application/json',
body: expect.stringContaining('RepositoryProfile'),
})
})
it('denies viewer writes', async () => {
const target = dependencies('viewer')
await expect(
createManualRepository(target, {
actor,
workspaceId,
displayName: 'Example',
profileDraft: profile(),
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
await expect(
appendRepositoryProfileRevision(target, {
actor,
workspaceId,
repositoryId,
expectedEtag: formatStrongProfileEtag(1, digest),
profileDraft: profile(),
}),
).rejects.toMatchObject({ code: 'workspace_access_denied' })
})
it.each(['editor', 'owner'] as const)(
'allows %s repository writes',
async (role) => {
const target = dependencies(role)
await expect(
createManualRepository(target, {
actor,
workspaceId,
displayName: 'Example',
profileDraft: profile(),
}),
).resolves.toMatchObject({ repository })
await expect(
appendRepositoryProfileRevision(target, {
actor,
workspaceId,
repositoryId,
expectedEtag: formatStrongProfileEtag(1, digest),
profileDraft: profile(),
}),
).resolves.toMatchObject({ created: false })
},
)
})
describe('repository application use cases', () => {
it('passes atomic manual creation only server-owned identity and revision metadata', async () => {
const target = dependencies('editor')
const supplied = profile()
await createManualRepository(target, {
actor,
workspaceId,
displayName: ' Operator name ',
profileDraft: supplied,
})
expect(target.store.createManualWithInitialProfile).toHaveBeenCalledOnce()
const request =
target.store.createManualWithInitialProfile.mock.calls[0]![0]
expect(request).toMatchObject({
workspaceId,
createdBy: userId,
displayName: 'Operator name',
defaultBranch: 'main',
initialProfile: {
metadata: {
name: 'Operator name',
source: 'manual',
revision: 1,
capturedAt: '2026-07-27T12:34:56.000Z',
},
},
})
expect(request.initialProfile.metadata.contentDigest).toBe(
digestRepositoryProfile(request.initialProfile),
)
expect(request.initialProfile.spec.commands[0]?.command).toContain(
'$(inert)',
)
expect(supplied.metadata).toMatchObject({
revision: 99,
source: 'gitea',
contentDigest: '0'.repeat(64),
})
})
it('preserves truthful imported provenance and its supplied capture time', async () => {
const target = dependencies('editor')
await createManualRepository(target, {
actor,
workspaceId,
displayName: 'Imported repository',
profileDraft: profile({
source: 'imported',
capturedAt: '2026-01-02T03:04:05.000Z',
}),
})
const request =
target.store.createManualWithInitialProfile.mock.calls[0]![0]
expect(request.initialProfile.metadata).toMatchObject({
source: 'imported',
capturedAt: '2026-01-02T03:04:05.000Z',
})
})
it('validates drafts without trusting their digest and strips server metadata before append', async () => {
const target = dependencies('editor')
await appendRepositoryProfileRevision(target, {
actor,
workspaceId,
repositoryId,
expectedEtag: formatStrongProfileEtag(1, digest),
profileDraft: profile(),
})
const request = target.store.appendImmutableRevision.mock.calls[0]![0]
expect(request.expected).toEqual({ revision: 1, contentDigest: digest })
expect(request.validatedDraft.metadata).not.toHaveProperty('revision')
expect(request.validatedDraft.metadata).not.toHaveProperty('contentDigest')
expect(request.createdBy).toBe(userId)
})
it('returns structured validation issues and never calls stores for invalid drafts', async () => {
const target = dependencies('editor')
await expect(
createManualRepository(target, {
actor,
workspaceId,
displayName: 'Example',
profileDraft: { kind: 'RepositoryProfile' },
}),
).rejects.toMatchObject({
code: 'repository_profile_invalid',
details: {
issues: expect.arrayContaining([
expect.objectContaining({
path: expect.any(String),
remediation: expect.any(String),
}),
]),
},
})
expect(target.store.createManualWithInitialProfile).not.toHaveBeenCalled()
})
it('exports deterministic JSON and YAML with the current strong ETag', async () => {
const target = dependencies('viewer')
const json = await exportCurrentRepositoryProfile(target, {
actor,
workspaceId,
repositoryId,
format: 'json',
})
const yaml = await exportCurrentRepositoryProfile(target, {
actor,
workspaceId,
repositoryId,
format: 'yaml',
})
expect(json.contentType).toBe('application/json')
expect(yaml.contentType).toBe('application/yaml')
expect(json.body.endsWith('\n')).toBe(true)
expect(yaml.body.endsWith('\n')).toBe(true)
expect(json.etag).toBe(yaml.etag)
expect(parseStrongProfileEtag(json.etag)).toEqual({
revision: 1,
contentDigest: revision().contentDigest,
})
})
it.each(['identity', 'current profile', 'append target'] as const)(
'uses one safe not-found error for a missing or cross-workspace %s',
async (kind) => {
const store = new MemoryStore()
if (kind === 'identity')
store.findByIdForWorkspace.mockResolvedValue(null)
if (kind === 'current profile')
store.findCurrentProfileForWorkspace.mockResolvedValue(null)
if (kind === 'append target')
store.appendImmutableRevision.mockResolvedValue(null)
const target = dependencies('owner', store)
const call =
kind === 'identity'
? getRepository(target, { actor, workspaceId, repositoryId })
: kind === 'current profile'
? getCurrentRepositoryProfile(target, {
actor,
workspaceId,
repositoryId,
})
: appendRepositoryProfileRevision(target, {
actor,
workspaceId,
repositoryId,
expectedEtag: formatStrongProfileEtag(1, digest),
profileDraft: profile(),
})
await expect(call).rejects.toMatchObject({
code: 'repository_not_found',
message: 'Repository not found',
details: {},
})
},
)
})
describe('strong repository profile ETags', () => {
it('round-trips the exact governed representation', () => {
const etag = `"profile:12:${digest}"`
expect(parseStrongProfileEtag(etag)).toEqual({
revision: 12,
contentDigest: digest,
})
expect(formatStrongProfileEtag(12, digest)).toBe(etag)
})
it.each([
'',
`profile:1:${digest}`,
`W/"profile:1:${digest}"`,
`"profile:0:${digest}"`,
`"profile:01:${digest}"`,
`"profile:1:${'A'.repeat(64)}"`,
`"profile:1:${'a'.repeat(63)}"`,
`"other:1:${digest}"`,
`"profile:9007199254740992:${digest}"`,
])('strictly rejects invalid ETag %s', (etag) => {
expect(() => parseStrongProfileEtag(etag)).toThrowError(
expect.objectContaining({ code: 'repository_profile_etag_invalid' }),
)
})
})
@@ -0,0 +1,398 @@
import {
applyRepositoryProfileServerMetadata,
exportRepositoryProfileJson,
exportRepositoryProfileYaml,
type RepositoryProfile,
type RepositoryProfileValidationIssue,
validateRepositoryProfile,
} from '@devrunbook/repository-intel'
import { DomainError } from '@devrunbook/domain'
import {
authorizeWorkspaceAction,
type AuthenticatedActor,
type WorkspaceAuthorizationLookup,
} from '../auth/workspace-authorization'
export type RepositoryIdentitySource = 'manual' | 'gitea'
export interface RepositorySummary {
readonly id: string
readonly displayName: string
readonly sourceType: RepositoryIdentitySource
readonly defaultBranch: string | null
readonly archived: boolean
readonly currentProfileRevision: number | null
readonly lastSnapshotAt: string | null
readonly createdAt: string
readonly updatedAt: string
}
export interface RepositoryProfileDraft {
readonly apiVersion: RepositoryProfile['apiVersion']
readonly kind: RepositoryProfile['kind']
readonly metadata: Omit<
RepositoryProfile['metadata'],
'revision' | 'contentDigest'
>
readonly spec: RepositoryProfile['spec']
}
export interface RepositoryProfileRevision {
readonly id: string
readonly repositoryId: string
readonly revisionNumber: number
readonly profile: RepositoryProfile
readonly contentDigest: string
readonly createdBy: string
readonly createdAt: string
}
export interface RepositoryPage {
readonly items: readonly RepositorySummary[]
readonly nextCursor: string | null
}
export interface RepositoryListQuery {
readonly q?: string
readonly cursor?: string | null
readonly source?: RepositoryIdentitySource
readonly archived?: boolean
readonly limit?: number
}
export interface StrongProfileEtag {
readonly revision: number
readonly contentDigest: string
}
export interface CreateManualRepositoryStoreRequest {
readonly workspaceId: string
readonly createdBy: string
readonly displayName: string
readonly defaultBranch: string | null
readonly initialProfile: RepositoryProfile
}
export interface AppendRepositoryProfileRevisionStoreRequest {
readonly workspaceId: string
readonly repositoryId: string
readonly createdBy: string
readonly expected: StrongProfileEtag
/**
* Structurally and semantically validated effective values. The application
* deliberately removes client revision/digest fields. The store must lock the
* current row, enforce expected, allocate the next revision, apply a new
* digest, and return the current row with created=false for a semantic no-op.
*/
readonly validatedDraft: RepositoryProfileDraft
}
export interface AppendRepositoryProfileRevisionStoreResult {
readonly revision: RepositoryProfileRevision
readonly created: boolean
}
export interface RepositoryStore {
listForWorkspace(
workspaceId: string,
query: RepositoryListQuery,
): Promise<RepositoryPage>
findByIdForWorkspace(
workspaceId: string,
repositoryId: string,
): Promise<RepositorySummary | null>
findCurrentProfileForWorkspace(
workspaceId: string,
repositoryId: string,
): Promise<RepositoryProfileRevision | null>
/** Atomically creates the manual identity and immutable revision 1. */
createManualWithInitialProfile(
request: CreateManualRepositoryStoreRequest,
): Promise<{
readonly repository: RepositorySummary
readonly revision: RepositoryProfileRevision
}>
/**
* Performs expected-ETag comparison and revision allocation under a row lock.
* Cross-workspace or missing identities return null without revealing which.
*/
appendImmutableRevision(
request: AppendRepositoryProfileRevisionStoreRequest,
): Promise<AppendRepositoryProfileRevisionStoreResult | null>
}
export interface RepositoryUseCaseDependencies {
readonly authorization: WorkspaceAuthorizationLookup
readonly store: RepositoryStore
readonly now: () => Date
}
export interface RepositoryActorRequest {
readonly actor: AuthenticatedActor | null
readonly workspaceId: string
}
export interface GetRepositoryRequest extends RepositoryActorRequest {
readonly repositoryId: string
}
export interface CreateManualRepositoryRequest extends RepositoryActorRequest {
readonly displayName: string
readonly profileDraft: unknown
}
export interface AppendRepositoryProfileRevisionRequest extends GetRepositoryRequest {
readonly expectedEtag: string
readonly profileDraft: unknown
}
export interface ExportCurrentRepositoryProfileRequest extends GetRepositoryRequest {
readonly format: 'json' | 'yaml'
}
export interface RepositoryProfileRevisionResult {
readonly revision: RepositoryProfileRevision
readonly etag: string
}
export interface AppendRepositoryProfileRevisionResult extends RepositoryProfileRevisionResult {
readonly created: boolean
}
function repositoryNotFound(): never {
throw new DomainError('repository_not_found', 'Repository not found')
}
function invalidProfile(
issues: readonly RepositoryProfileValidationIssue[],
): never {
throw new DomainError(
'repository_profile_invalid',
'Repository profile is invalid',
{ issues },
)
}
function validatedProfile(value: unknown): RepositoryProfile {
const result = validateRepositoryProfile(value, { verifyDigest: false })
if (!result.valid) invalidProfile(result.issues)
return result.profile
}
function withoutServerMetadata(
profile: RepositoryProfile,
): RepositoryProfileDraft {
const metadata: RepositoryProfileDraft['metadata'] = {
name: profile.metadata.name,
source: profile.metadata.source,
...(profile.metadata.capturedAt !== undefined
? { capturedAt: profile.metadata.capturedAt }
: {}),
...(profile.metadata.sourceReference !== undefined
? { sourceReference: profile.metadata.sourceReference }
: {}),
}
return {
apiVersion: profile.apiVersion,
kind: profile.kind,
metadata,
spec: profile.spec,
}
}
function assertDisplayName(value: string): string {
const displayName = value.trim()
if (displayName.length === 0 || displayName.length > 120) {
throw new DomainError(
'repository_input_invalid',
'Repository input is invalid',
{
issues: [
{
path: '/displayName',
code: 'display_name_invalid',
message: 'Display name must contain between 1 and 120 characters',
remediation: 'Provide the operator-visible repository name.',
},
],
},
)
}
return displayName
}
export function formatStrongProfileEtag(
revision: number,
contentDigest: string,
): string {
if (!Number.isInteger(revision) || revision < 1) {
throw new RangeError('Profile ETag revision must be a positive integer')
}
if (!/^[a-f0-9]{64}$/u.test(contentDigest)) {
throw new TypeError('Profile ETag digest must be lowercase SHA-256')
}
return `"profile:${revision}:${contentDigest}"`
}
export function parseStrongProfileEtag(value: string): StrongProfileEtag {
const match = /^"profile:([1-9]\d*):([a-f0-9]{64})"$/u.exec(value)
if (!match) {
throw new DomainError(
'repository_profile_etag_invalid',
'A valid current profile ETag is required',
)
}
const revision = Number(match[1])
if (!Number.isSafeInteger(revision)) {
throw new DomainError(
'repository_profile_etag_invalid',
'A valid current profile ETag is required',
)
}
return { revision, contentDigest: match[2]! }
}
async function authorize(
dependencies: RepositoryUseCaseDependencies,
request: RepositoryActorRequest,
action: 'read' | 'write',
): Promise<string> {
const context = await authorizeWorkspaceAction(dependencies.authorization, {
actor: request.actor,
workspaceId: request.workspaceId,
action,
})
return context.userId
}
export async function listRepositories(
dependencies: RepositoryUseCaseDependencies,
request: RepositoryActorRequest & { readonly query?: RepositoryListQuery },
): Promise<RepositoryPage> {
await authorize(dependencies, request, 'read')
return dependencies.store.listForWorkspace(
request.workspaceId,
request.query ?? {},
)
}
export async function getRepository(
dependencies: RepositoryUseCaseDependencies,
request: GetRepositoryRequest,
): Promise<RepositorySummary> {
await authorize(dependencies, request, 'read')
return (
(await dependencies.store.findByIdForWorkspace(
request.workspaceId,
request.repositoryId,
)) ?? repositoryNotFound()
)
}
export async function createManualRepository(
dependencies: RepositoryUseCaseDependencies,
request: CreateManualRepositoryRequest,
): Promise<{
readonly repository: RepositorySummary
readonly revision: RepositoryProfileRevision
readonly etag: string
}> {
const createdBy = await authorize(dependencies, request, 'write')
const displayName = assertDisplayName(request.displayName)
const supplied = validatedProfile(request.profileDraft)
const suppliedMetadata = withoutServerMetadata(supplied).metadata
const source = suppliedMetadata.source === 'imported' ? 'imported' : 'manual'
const capturedAt =
source === 'imported' && suppliedMetadata.capturedAt
? suppliedMetadata.capturedAt
: dependencies.now().toISOString()
const manualProfile: RepositoryProfile = {
...supplied,
metadata: {
...suppliedMetadata,
name: displayName,
source,
capturedAt,
revision: 1,
},
}
const initialProfile = applyRepositoryProfileServerMetadata(manualProfile, 1)
const result = await dependencies.store.createManualWithInitialProfile({
workspaceId: request.workspaceId,
createdBy,
displayName,
defaultBranch: initialProfile.spec.defaultBranch ?? null,
initialProfile,
})
return {
...result,
etag: formatStrongProfileEtag(
result.revision.revisionNumber,
result.revision.contentDigest,
),
}
}
export async function getCurrentRepositoryProfile(
dependencies: RepositoryUseCaseDependencies,
request: GetRepositoryRequest,
): Promise<RepositoryProfileRevisionResult> {
await authorize(dependencies, request, 'read')
const revision = await dependencies.store.findCurrentProfileForWorkspace(
request.workspaceId,
request.repositoryId,
)
if (!revision) repositoryNotFound()
return {
revision,
etag: formatStrongProfileEtag(
revision.revisionNumber,
revision.contentDigest,
),
}
}
export async function appendRepositoryProfileRevision(
dependencies: RepositoryUseCaseDependencies,
request: AppendRepositoryProfileRevisionRequest,
): Promise<AppendRepositoryProfileRevisionResult> {
const createdBy = await authorize(dependencies, request, 'write')
const expected = parseStrongProfileEtag(request.expectedEtag)
const profile = validatedProfile(request.profileDraft)
const result = await dependencies.store.appendImmutableRevision({
workspaceId: request.workspaceId,
repositoryId: request.repositoryId,
createdBy,
expected,
validatedDraft: withoutServerMetadata(profile),
})
if (!result) repositoryNotFound()
return {
...result,
etag: formatStrongProfileEtag(
result.revision.revisionNumber,
result.revision.contentDigest,
),
}
}
export async function exportCurrentRepositoryProfile(
dependencies: RepositoryUseCaseDependencies,
request: ExportCurrentRepositoryProfileRequest,
): Promise<{
readonly contentType: 'application/json' | 'application/yaml'
readonly body: string
readonly etag: string
}> {
const current = await getCurrentRepositoryProfile(dependencies, request)
return {
contentType:
request.format === 'json' ? 'application/json' : 'application/yaml',
body:
request.format === 'json'
? exportRepositoryProfileJson(current.revision.profile)
: exportRepositoryProfileYaml(current.revision.profile),
etag: current.etag,
}
}
@@ -0,0 +1,38 @@
import { describe, expect, it, vi } from 'vitest'
import { enforceArtifactRetention } from './artifact-retention'
describe('artifact retention', () => {
it('removes only store-selected expired bytes and finalizes their metadata', async () => {
const artifacts = [
{
id: 'artifact-1',
workspaceId: 'workspace-1',
storageKey: 'a'.repeat(64),
},
{
id: 'artifact-2',
workspaceId: 'workspace-1',
storageKey: 'b'.repeat(64),
},
]
const store = {
listExpired: vi.fn().mockResolvedValue(artifacts),
finalizeDeletion: vi.fn().mockResolvedValue(true),
}
const storage = {
deleteIfPresent: vi
.fn()
.mockResolvedValueOnce(true)
.mockResolvedValueOnce(false),
}
await expect(
enforceArtifactRetention({
store,
storage,
now: new Date('2026-07-27T12:00:00.000Z'),
}),
).resolves.toEqual({ scanned: 2, deleted: 1, missing: 1 })
expect(store.finalizeDeletion).toHaveBeenCalledTimes(2)
})
})
@@ -0,0 +1,43 @@
export interface ExpiredArtifactCandidate {
readonly id: string
readonly workspaceId: string
readonly storageKey: string
}
export interface ArtifactRetentionStore {
listExpired(
now: Date,
limit: number,
): Promise<readonly ExpiredArtifactCandidate[]>
finalizeDeletion(input: {
readonly artifact: ExpiredArtifactCandidate
readonly now: Date
}): Promise<boolean>
}
export interface ArtifactRetentionStorage {
deleteIfPresent(storageKey: string): Promise<boolean>
}
export async function enforceArtifactRetention(input: {
readonly store: ArtifactRetentionStore
readonly storage: ArtifactRetentionStorage
readonly now?: Date
readonly limit?: number
}) {
const now = input.now ?? new Date()
const limit = input.limit ?? 500
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5_000) {
throw new Error('Artifact retention batch limit must be between 1 and 5000')
}
const candidates = await input.store.listExpired(now, limit)
let deleted = 0
let missing = 0
for (const artifact of candidates) {
const removed = await input.storage.deleteIfPresent(artifact.storageKey)
if (removed) deleted += 1
else missing += 1
await input.store.finalizeDeletion({ artifact, now })
}
return Object.freeze({ scanned: candidates.length, deleted, missing })
}
@@ -0,0 +1,55 @@
import { describe, expect, it, vi } from 'vitest'
import {
completeFirstRun,
type FirstRunStore,
type FirstRunTransaction,
} from './complete-first-run'
function fixture(imported = 28) {
const transaction: FirstRunTransaction = {
isSetupComplete: vi.fn().mockResolvedValue(false),
createOwner: vi.fn().mockResolvedValue({ id: 'owner-1' }),
createPersonalWorkspace: vi.fn().mockResolvedValue({ id: 'workspace-1' }),
addOwnerMembership: vi.fn().mockResolvedValue(undefined),
importBuiltInPlaybooks: vi.fn().mockResolvedValue({ imported }),
completeSetup: vi.fn().mockResolvedValue(undefined),
appendAuditEvent: vi.fn().mockResolvedValue(undefined),
}
const store: FirstRunStore = {
withSetupLock: (work) => work(transaction),
}
return { store, transaction }
}
const request = {
instanceName: 'DevRunbook',
publicBaseUrl: 'http://localhost:3000',
owner: {
email: 'owner@example.test',
displayName: 'Owner',
passwordHash: '[redacted-hash]',
},
configuration: {},
configurationDigest: 'a'.repeat(64),
}
describe('completeFirstRun', () => {
it('completes identity, workspace, catalog and audit inside the setup lock', async () => {
const { store, transaction } = fixture()
await expect(completeFirstRun(store, request)).resolves.toEqual({
ownerId: 'owner-1',
workspaceId: 'workspace-1',
importedPlaybooks: 28,
})
expect(transaction.completeSetup).toHaveBeenCalledOnce()
expect(transaction.appendAuditEvent).toHaveBeenCalledOnce()
})
it('fails closed when the complete built-in catalog cannot import', async () => {
const { store, transaction } = fixture(27)
await expect(completeFirstRun(store, request)).rejects.toMatchObject({
code: 'catalog_import_incomplete',
})
expect(transaction.completeSetup).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,97 @@
import { DomainError } from '@devrunbook/domain'
export interface FirstRunOwner {
email: string
displayName: string
passwordHash: string
}
export interface CompleteFirstRunRequest {
instanceName: string
publicBaseUrl: string
owner: FirstRunOwner
configuration: Readonly<Record<string, unknown>>
configurationDigest: string
}
export interface FirstRunTransaction {
isSetupComplete(): Promise<boolean>
createOwner(owner: FirstRunOwner): Promise<{ id: string }>
createPersonalWorkspace(input: {
ownerId: string
name: string
}): Promise<{ id: string }>
addOwnerMembership(input: {
ownerId: string
workspaceId: string
}): Promise<void>
importBuiltInPlaybooks(): Promise<{ imported: number }>
completeSetup(input: {
ownerId: string
configuration: Readonly<Record<string, unknown>>
configurationDigest: string
}): Promise<void>
appendAuditEvent(input: {
actorUserId: string
workspaceId: string
action: 'instance.setup.completed'
}): Promise<void>
}
export interface FirstRunStore {
withSetupLock<T>(
work: (transaction: FirstRunTransaction) => Promise<T>,
): Promise<T>
}
export interface FirstRunResult {
ownerId: string
workspaceId: string
importedPlaybooks: number
}
export async function completeFirstRun(
store: FirstRunStore,
request: CompleteFirstRunRequest,
): Promise<FirstRunResult> {
return store.withSetupLock(async (transaction) => {
if (await transaction.isSetupComplete()) {
throw new DomainError(
'setup_already_complete',
'Initial setup is already complete',
)
}
const owner = await transaction.createOwner(request.owner)
const workspace = await transaction.createPersonalWorkspace({
ownerId: owner.id,
name: `${request.owner.displayName}'s workspace`,
})
await transaction.addOwnerMembership({
ownerId: owner.id,
workspaceId: workspace.id,
})
const catalog = await transaction.importBuiltInPlaybooks()
if (catalog.imported !== 28) {
throw new DomainError(
'catalog_import_incomplete',
`Setup requires 28 built-in playbooks; imported ${catalog.imported}`,
)
}
await transaction.completeSetup({
ownerId: owner.id,
configuration: request.configuration,
configurationDigest: request.configurationDigest,
})
await transaction.appendAuditEvent({
actorUserId: owner.id,
workspaceId: workspace.id,
action: 'instance.setup.completed',
})
return {
ownerId: owner.id,
workspaceId: workspace.id,
importedPlaybooks: catalog.imported,
}
})
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"]
}