From 1936b6aafd79f707878714d07abdf8183d1161af Mon Sep 17 00:00:00 2001 From: egallmann Date: Thu, 13 Aug 2026 22:10:00 -0400 Subject: [PATCH 1/6] chore(architecture): synchronize generated baseline --- adrs/index/architecture-index.yaml | 2 +- adrs/index/entity-registry.yaml | 9 +++++---- adrs/index/invariant-registry.yaml | 9 +++++---- adrs/manifest.yaml | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/adrs/index/architecture-index.yaml b/adrs/index/architecture-index.yaml index fdf8c80..68d8fb3 100644 --- a/adrs/index/architecture-index.yaml +++ b/adrs/index/architecture-index.yaml @@ -1,7 +1,7 @@ schema_version: '1.1' type: architecture_index architecture_namespace: ste-runtime -generated_at: '2026-08-13T00:43:39Z' +generated_at: '2026-08-14T02:09:04Z' generator: adr-architecture-index entity_registry_path: adrs/index/entity-registry.yaml relationship_registry_path: adrs/index/relationship-registry.yaml diff --git a/adrs/index/entity-registry.yaml b/adrs/index/entity-registry.yaml index 42037cc..5de8851 100644 --- a/adrs/index/entity-registry.yaml +++ b/adrs/index/entity-registry.yaml @@ -5588,8 +5588,8 @@ entities: entity_type: invariant name: 019ff84e-4ece-7387-b33f-3d203e3c968c summary: >- - Single repository only: RECON discovers files within the current repository. Cross-repository reconciliation is - out of scope. + RECON extraction is repository-local: each repository observation discovers files within its registered repository + source. Workspace orchestration may compose multiple repository observations into one derived workspace p lifecycle_stage: active admission_status: admitted canonical_source: @@ -5601,8 +5601,9 @@ entities: adr_id: 019ff84e-4ece-791b-822f-21f537c95340 scope: global statement: >- - Single repository only: RECON discovers files within the current repository. Cross-repository reconciliation is - out of scope. + RECON extraction is repository-local: each repository observation discovers files within its registered + repository source. Workspace orchestration may compose multiple repository observations into one derived + workspace projection. Cross-workspace reconciliation remains out of scope unless explicitly federated. enforcement_level: must declaration_mode: local upheld_by_decisions: [] diff --git a/adrs/index/invariant-registry.yaml b/adrs/index/invariant-registry.yaml index 1077c12..667a715 100644 --- a/adrs/index/invariant-registry.yaml +++ b/adrs/index/invariant-registry.yaml @@ -224,8 +224,8 @@ entities: entity_type: invariant name: 019ff84e-4ece-7387-b33f-3d203e3c968c summary: >- - Single repository only: RECON discovers files within the current repository. Cross-repository reconciliation is - out of scope. + RECON extraction is repository-local: each repository observation discovers files within its registered repository + source. Workspace orchestration may compose multiple repository observations into one derived workspace p lifecycle_stage: active admission_status: admitted canonical_source: @@ -237,8 +237,9 @@ entities: adr_id: 019ff84e-4ece-791b-822f-21f537c95340 scope: global statement: >- - Single repository only: RECON discovers files within the current repository. Cross-repository reconciliation is - out of scope. + RECON extraction is repository-local: each repository observation discovers files within its registered + repository source. Workspace orchestration may compose multiple repository observations into one derived + workspace projection. Cross-workspace reconciliation remains out of scope unless explicitly federated. enforcement_level: must declaration_mode: local upheld_by_decisions: [] diff --git a/adrs/manifest.yaml b/adrs/manifest.yaml index 7da27a5..631eb94 100644 --- a/adrs/manifest.yaml +++ b/adrs/manifest.yaml @@ -1,6 +1,6 @@ schema_version: '1.0' type: manifest -generated_date: '2026-08-13T00:43:39Z' +generated_date: '2026-08-14T02:09:04Z' generated_from: adrs/**/*.yaml adrs: - id: 019ff84e-4ece-70ba-bf2e-a0fecd4a986e From 3919b77f4daa54b5e6c8f1f85d05a3f281785c2d Mon Sep 17 00:00:00 2001 From: egallmann Date: Thu, 13 Aug 2026 22:27:31 -0400 Subject: [PATCH 2/6] test(runtime): specify private workspace execution boundary --- src/public/runtime.test.ts | 28 +++++++++++ .../runtime-workspace-execution.test.ts | 49 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/workspace/runtime-workspace-execution.test.ts diff --git a/src/public/runtime.test.ts b/src/public/runtime.test.ts index aeb6fe6..b69aa65 100644 --- a/src/public/runtime.test.ts +++ b/src/public/runtime.test.ts @@ -330,4 +330,32 @@ describe('no-source current projection', () => { await fs.rm(repositoryB, { recursive: true, force: true }); } }); + + it('isolates concurrent refreshes without materializing state in either source repository', async () => { + const repositoryA = await createFixtureRepository('ste-runtime-concurrent-a'); + const repositoryB = await createFixtureRepository('ste-runtime-concurrent-b'); + const runtime = createRuntime(); + try { + const [registrationA, registrationB] = await Promise.all([ + runtime.createRegistration({ repositories: [{ source: { kind: 'local', path: repositoryA } }] }), + runtime.createRegistration({ repositories: [{ source: { kind: 'local', path: repositoryB } }] }), + ]); + const [snapshotA, snapshotB] = await Promise.all([ + (await runtime.open(registrationA)).refresh(), + (await runtime.open(registrationB)).refresh(), + ]); + + expect(snapshotA.workspaceId).toBe(registrationA.workspaceId); + expect(snapshotB.workspaceId).toBe(registrationB.workspaceId); + expect(snapshotA.workspaceId).not.toBe(snapshotB.workspaceId); + for (const repository of [repositoryA, repositoryB]) { + await expect(fs.access(path.join(repository, '.ste'))).rejects.toMatchObject({ code: 'ENOENT' }); + await expect(fs.access(path.join(repository, '.workspace-graph'))).rejects.toMatchObject({ code: 'ENOENT' }); + } + } finally { + await runtime.close(); + await fs.rm(repositoryA, { recursive: true, force: true }); + await fs.rm(repositoryB, { recursive: true, force: true }); + } + }); }); diff --git a/src/workspace/runtime-workspace-execution.test.ts b/src/workspace/runtime-workspace-execution.test.ts new file mode 100644 index 0000000..c1c0312 --- /dev/null +++ b/src/workspace/runtime-workspace-execution.test.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { createRuntime } from '../public/runtime.js'; +import { createWorkspaceExecutionAdapter } from './runtime-workspace-execution.js'; + +async function createRepository(prefix: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`)); + await fs.mkdir(path.join(root, 'src'), { recursive: true }); + await fs.writeFile(path.join(root, 'package.json'), '{"name":"workspace-execution-fixture","version":"1.0.0"}\n'); + await fs.writeFile(path.join(root, 'src', 'index.ts'), 'export const fixture = 1;\n'); + return root; +} + +describe('private runtime workspace execution adapter', () => { + it('owns synthetic execution mapping and returns an in-memory graph result', async () => { + const repositoryA = await createRepository('runtime-workspace-execution-a'); + const repositoryB = await createRepository('runtime-workspace-execution-b'); + const runtime = createRuntime(); + const adapter = createWorkspaceExecutionAdapter(); + + try { + const registration = await runtime.createRegistration({ + repositories: [ + { source: { kind: 'local', path: repositoryB } }, + { source: { kind: 'local', path: repositoryA } }, + ], + }); + + const result = await adapter.execute(registration); + + expect([...result.executionToRepository.keys()]).toEqual(['repo-1', 'repo-2']); + expect(new Set(result.executionToRepository.values())).toEqual( + new Set(registration.definition.repositories.map(repository => repository.repositoryId)), + ); + expect([...result.initialStatuses.values()]).toEqual(['present', 'present']); + expect(result.reconResult.repos.every(repository => repository.status === 'success')).toBe(true); + expect(result.graph.nodes).toEqual(expect.any(Array)); + } finally { + await adapter.close(); + await runtime.close(); + await fs.rm(repositoryA, { recursive: true, force: true }); + await fs.rm(repositoryB, { recursive: true, force: true }); + } + }); +}); From 5044cdaa95048ade334e470e44590e22a2ab320c Mon Sep 17 00:00:00 2001 From: egallmann Date: Thu, 13 Aug 2026 22:28:40 -0400 Subject: [PATCH 3/6] feat(runtime): isolate public workspace execution --- src/public/runtime.ts | 151 +++----------- src/workspace/runtime-workspace-execution.ts | 198 +++++++++++++++++++ 2 files changed, 222 insertions(+), 127 deletions(-) create mode 100644 src/workspace/runtime-workspace-execution.ts diff --git a/src/public/runtime.ts b/src/public/runtime.ts index 8aae5d5..d7f0fbf 100644 --- a/src/public/runtime.ts +++ b/src/public/runtime.ts @@ -1,10 +1,11 @@ import crypto from 'node:crypto'; -import fs from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import type { WorkspaceReconResult } from '../workspace/workspace-recon.js'; +import { + createWorkspaceExecutionAdapter, + type LegacyGraph, + type WorkspaceExecutionResult, + WorkspaceExecutionError, +} from '../workspace/runtime-workspace-execution.js'; import { canonicalDefinition, definitionRevision, @@ -38,38 +39,6 @@ import type { WorkspaceRegistration, } from './types.js'; -interface LegacyNode { - id: string; - type: string; - name: string; - repo?: string; - attributes?: Record; - provenance?: { - source_path?: string; - source_ref?: string; - repo?: string; - }; -} - -interface LegacyEdge { - from: string; - to: string; - verb: string; - provenance?: { - source_path?: string; - source_ref?: string; - repo?: string; - source_repo?: string; - target_repo?: string; - evidence?: string; - }; -} - -interface LegacyGraph { - nodes?: LegacyNode[]; - edges?: LegacyEdge[]; -} - const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; function uuidv7(): string { @@ -195,10 +164,6 @@ function validateRegistration(registration: WorkspaceRegistration): WorkspaceReg return buildRegistration(registration.workspaceId, normalizedDefinition, registration.display, registration.repositories); } -function executionKey(index: number): string { - return `repo-${index + 1}`; -} - function sourceLocator(sourcePath?: string, sourceRef?: string): string | undefined { if (!sourcePath && !sourceRef) return undefined; if (!sourceRef) return sourcePath; @@ -213,22 +178,6 @@ function repositoryIdFor( return key ? executionToRepository.get(key) : undefined; } -async function readLegacyGraph(raw: string): Promise { - const { default: yaml } = await import('js-yaml'); - const parsed = yaml.load(raw) as LegacyGraph | null; - return parsed ?? {}; -} - -async function pathStatus(sourcePath: string): Promise<'present' | 'orphaned' | 'unavailable'> { - try { - const stat = await fs.stat(sourcePath); - return stat.isDirectory() ? 'present' : 'unavailable'; - } catch (error) { - const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : ''; - return code === 'ENOENT' || code === 'ENOTDIR' ? 'orphaned' : 'unavailable'; - } -} - function toDiagnostic(code: string, message: string, repositoryIds?: readonly RepositoryId[]): RuntimeDiagnostic { return { code, message, repositoryIds }; } @@ -331,44 +280,16 @@ function currentObservationValue( }; } -async function writeWorkspaceManifest( - workspaceRoot: string, - registration: WorkspaceRegistration, -): Promise<{ manifestPath: string; executionToRepository: Map }> { - const repositories = sortedByRepositoryId(registration.definition.repositories); - const executionToRepository = new Map(); - const manifestRepos = repositories.map((repository, index) => { - const key = executionKey(index); - executionToRepository.set(key, repository.repositoryId); - return { - name: key, - path: repository.source.path, - kind: 'service', - lang: 'unknown', - }; - }); - const manifestPath = path.join(workspaceRoot, 'workspace.yaml'); - const { default: yaml } = await import('js-yaml'); - await fs.writeFile( - manifestPath, - yaml.dump({ schema_version: '1.0', output_dir: '.workspace-graph', repos: manifestRepos }), - 'utf8', - ); - return { manifestPath, executionToRepository }; -} - function observationsForResult( registration: WorkspaceRegistration, - result: WorkspaceReconResult, - executionToRepository: ReadonlyMap, - initialStatuses: ReadonlyMap, + execution: WorkspaceExecutionResult, ): { observations: RepositoryObservation[]; observedCount: number; diagnostics: RuntimeDiagnostic[] } { const observations: RepositoryObservation[] = []; const diagnostics: RuntimeDiagnostic[] = []; for (const repository of sortedByRepositoryId(registration.definition.repositories)) { - const key = [...executionToRepository.entries()].find(([, id]) => id === repository.repositoryId)?.[0]; - const repoResult = result.repos.find(entry => entry.name === key); - const initial = initialStatuses.get(repository.repositoryId) ?? 'unavailable'; + const key = [...execution.executionToRepository.entries()].find(([, id]) => id === repository.repositoryId)?.[0]; + const repoResult = execution.reconResult.repos.find(entry => entry.name === key); + const initial = execution.initialStatuses.get(repository.repositoryId) ?? 'unavailable'; if (repoResult?.status === 'success') { observations.push({ repositoryId: repository.repositoryId, @@ -390,7 +311,7 @@ function observationsForResult( } export function createRuntime(): Runtime { - const temporaryRoots = new Set(); + const workspaceExecution = createWorkspaceExecutionAdapter(); let closed = false; const capabilities: RuntimeCapabilityManifest = Object.freeze({ @@ -484,42 +405,24 @@ export function createRuntime(): Runtime { registration, refresh: async (): Promise => { ensureOpen(); - const refreshRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'ste-runtime-public-refresh-')); - temporaryRoots.add(refreshRoot); - const { manifestPath, executionToRepository } = await writeWorkspaceManifest(refreshRoot, registration); - const initialStatuses = new Map(); - for (const repository of registration.definition.repositories) { - initialStatuses.set(repository.repositoryId, await pathStatus(repository.source.path)); - } - try { - const [{ executeWorkspaceRecon }, { preflightWorkspaceGraphIdentity }] = await Promise.all([ - import('../workspace/workspace-recon.js'), - import('../workspace/workspace-merge.js'), - ]); - const result = await executeWorkspaceRecon({ - workspacePath: manifestPath, - mode: 'full', - runtimeDir: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'), - failOnAnyError: false, - skipUnchanged: false, - beforeMerge: outputDir => preflightWorkspaceGraphIdentity(outputDir), - }); + const execution = await workspaceExecution.execute(registration); const { observations, observedCount, diagnostics } = observationsForResult( registration, - result, - executionToRepository, - initialStatuses, + execution, ); if (observedCount === 0) { throw new RefreshError('NO_SOURCE_OBSERVED', 'No registered repository could be observed', diagnostics); } - const graphRaw = await fs.readFile(path.join(refreshRoot, '.workspace-graph', 'graph.yaml'), 'utf8'); - const legacyGraph = await readLegacyGraph(graphRaw); const snapshotId = uuidv7() as SnapshotId; validateSnapshotId(snapshotId); - const graph = graphFromLegacy(registration.workspaceId, snapshotId, legacyGraph, executionToRepository); + const graph = graphFromLegacy( + registration.workspaceId, + snapshotId, + execution.graph, + execution.executionToRepository, + ); const snapshot: RuntimeSnapshot = { snapshotId, workspaceId: registration.workspaceId, @@ -535,22 +438,17 @@ export function createRuntime(): Runtime { return deepFreeze(snapshot); } catch (error) { if (error instanceof RefreshError) throw error; - if (error instanceof Error && error.name === 'WorkspaceIdentityCollisionError' && 'collisions' in error) { - const collisions = (error as Error & { collisions: Array<{ id: string; repositories: string[] }> }).collisions; + if (error instanceof WorkspaceExecutionError && error.code === 'ENTITY_ID_COLLISION') { + const collisions = error.collisions ?? []; const diagnostics = collisions.map(collision => toDiagnostic( 'ENTITY_ID_COLLISION', - `${collision.id} was declared by ${collision.repositories.join(', ')}`, - collision.repositories - .map(repository => executionToRepository.get(repository)) - .filter((repositoryId): repositoryId is RepositoryId => repositoryId !== undefined), + `${collision.id} was declared by ${collision.repositoryIds.join(', ')}`, + collision.repositoryIds, )); throw new RefreshError('ENTITY_ID_COLLISION', error.message, diagnostics); } const message = error instanceof Error ? error.message : String(error); throw new RefreshError('REFRESH_FAILED', message, [toDiagnostic('REFRESH_FAILED', message)]); - } finally { - await fs.rm(refreshRoot, { recursive: true, force: true }); - temporaryRoots.delete(refreshRoot); } }, }; @@ -564,8 +462,7 @@ export function createRuntime(): Runtime { close: async () => { if (closed) return; closed = true; - await Promise.all([...temporaryRoots].map(root => fs.rm(root, { recursive: true, force: true }))); - temporaryRoots.clear(); + await workspaceExecution.close(); }, }; } diff --git a/src/workspace/runtime-workspace-execution.ts b/src/workspace/runtime-workspace-execution.ts new file mode 100644 index 0000000..901fd44 --- /dev/null +++ b/src/workspace/runtime-workspace-execution.ts @@ -0,0 +1,198 @@ +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import yaml from 'js-yaml'; + +import { implements_adr } from '../architecture/intent-decorators.js'; +import type { + RepositoryId, + WorkspaceRegistration, +} from '../public/types.js'; +import { + preflightWorkspaceGraphIdentity, + WorkspaceIdentityCollisionError, +} from './workspace-merge.js'; +import { + executeWorkspaceRecon, + type WorkspaceReconResult, +} from './workspace-recon.js'; + +export interface LegacyGraph { + nodes?: Array<{ + id: string; + type: string; + name: string; + repo?: string; + attributes?: Record; + provenance?: { + source_path?: string; + source_ref?: string; + repo?: string; + }; + }>; + edges?: Array<{ + from: string; + to: string; + verb: string; + provenance?: { + source_path?: string; + source_ref?: string; + repo?: string; + source_repo?: string; + target_repo?: string; + evidence?: string; + }; + }>; +} + +export type SourceAvailability = 'present' | 'orphaned' | 'unavailable'; + +export interface WorkspaceExecutionResult { + readonly executionToRepository: ReadonlyMap; + readonly graph: LegacyGraph; + readonly initialStatuses: ReadonlyMap; + readonly reconResult: WorkspaceReconResult; +} + +export class WorkspaceExecutionError extends Error { + readonly code: 'ENTITY_ID_COLLISION' | 'EXECUTION_FAILED'; + readonly collisions?: readonly { id: string; repositoryIds: readonly RepositoryId[] }[]; + + constructor( + code: WorkspaceExecutionError['code'], + message: string, + collisions?: readonly { id: string; repositoryIds: readonly RepositoryId[] }[], + ) { + super(message); + this.name = 'WorkspaceExecutionError'; + this.code = code; + this.collisions = collisions; + } +} + +export interface WorkspaceExecutionAdapter { + execute(registration: WorkspaceRegistration): Promise; + close(): Promise; +} + +function executionKey(index: number): string { + return `repo-${index + 1}`; +} + +function sortedByRepositoryId(values: readonly T[]): T[] { + return [...values].sort((a, b) => a.repositoryId.localeCompare(b.repositoryId)); +} + +async function pathStatus(sourcePath: string): Promise { + try { + const stat = await fs.stat(sourcePath); + return stat.isDirectory() ? 'present' : 'unavailable'; + } catch (error) { + const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : ''; + return code === 'ENOENT' || code === 'ENOTDIR' ? 'orphaned' : 'unavailable'; + } +} + +async function writeWorkspaceManifest( + workspaceRoot: string, + registration: WorkspaceRegistration, +): Promise<{ manifestPath: string; executionToRepository: Map }> { + const executionToRepository = new Map(); + const manifestRepos = sortedByRepositoryId(registration.definition.repositories).map((repository, index) => { + const key = executionKey(index); + executionToRepository.set(key, repository.repositoryId); + return { + name: key, + path: repository.source.path, + kind: 'service', + lang: 'unknown', + }; + }); + const manifestPath = path.join(workspaceRoot, 'workspace.yaml'); + await fs.writeFile( + manifestPath, + yaml.dump({ schema_version: '1.0', output_dir: '.workspace-graph', repos: manifestRepos }), + 'utf8', + ); + return { manifestPath, executionToRepository }; +} + +function readLegacyGraph(raw: string): LegacyGraph { + return (yaml.load(raw) as LegacyGraph | null) ?? {}; +} + +/** + * Private P1 execution boundary. It owns all temporary legacy workspace state + * and returns only parsed, in-memory data to the public runtime facade. + */ +export const createWorkspaceExecutionAdapter: () => WorkspaceExecutionAdapter = implements_adr( + '019ff84e-4ece-7ddc-b31f-3a009abe14b3', +)(function createWorkspaceExecutionAdapter(): WorkspaceExecutionAdapter { + const temporaryRoots = new Set(); + let closed = false; + + const ensureOpen = (): void => { + if (closed) throw new WorkspaceExecutionError('EXECUTION_FAILED', 'Workspace execution adapter has been closed'); + }; + + return { + execute: async (registration: WorkspaceRegistration): Promise => { + ensureOpen(); + const refreshRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'ste-runtime-public-refresh-')); + temporaryRoots.add(refreshRoot); + let executionToRepository: Map | undefined; + + try { + const manifest = await writeWorkspaceManifest(refreshRoot, registration); + executionToRepository = manifest.executionToRepository; + const initialStatuses = new Map(); + for (const repository of registration.definition.repositories) { + initialStatuses.set(repository.repositoryId, await pathStatus(repository.source.path)); + } + + const reconResult = await executeWorkspaceRecon({ + workspacePath: manifest.manifestPath, + mode: 'full', + runtimeDir: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'), + failOnAnyError: false, + skipUnchanged: false, + beforeMerge: outputDir => preflightWorkspaceGraphIdentity(outputDir), + }); + const graphRaw = await fs.readFile(path.join(refreshRoot, '.workspace-graph', 'graph.yaml'), 'utf8'); + return { + executionToRepository: manifest.executionToRepository, + graph: readLegacyGraph(graphRaw), + initialStatuses, + reconResult, + }; + } catch (error) { + if (error instanceof WorkspaceIdentityCollisionError) { + throw new WorkspaceExecutionError( + 'ENTITY_ID_COLLISION', + error.message, + error.collisions.map(collision => ({ + id: collision.id, + repositoryIds: collision.repositories + .map(repository => executionToRepository?.get(repository)) + .filter((repositoryId): repositoryId is RepositoryId => repositoryId !== undefined), + })), + ); + } + if (error instanceof WorkspaceExecutionError) throw error; + const message = error instanceof Error ? error.message : String(error); + throw new WorkspaceExecutionError('EXECUTION_FAILED', message); + } finally { + await fs.rm(refreshRoot, { recursive: true, force: true }); + temporaryRoots.delete(refreshRoot); + } + }, + close: async (): Promise => { + if (closed) return; + closed = true; + await Promise.all([...temporaryRoots].map(root => fs.rm(root, { recursive: true, force: true }))); + temporaryRoots.clear(); + }, + }; +}); From 352136fa72c4b808c320ee433eb89092d51e66d5 Mon Sep 17 00:00:00 2001 From: egallmann Date: Thu, 13 Aug 2026 22:29:49 -0400 Subject: [PATCH 4/6] docs(runtime): align supported public and RSS guidance --- README.md | 30 +++++++++++++++------------- instructions/RSS-PROGRAMMATIC-API.md | 18 ++++++++++------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index df5e5e5..42f95a9 100644 --- a/README.md +++ b/README.md @@ -185,24 +185,26 @@ before connecting an editor. ## Programmatic Use -After building a source checkout, the current implementation exports can be -imported from `dist/index.js`: +The supported package-root API is the P1 runtime contract: ```js -import { initRssContext, search, blastRadius } from './dist/index.js'; - -const ctx = await initRssContext('.ste/state'); -const matches = search(ctx, 'authentication'); -const impact = matches.nodes[0] - ? blastRadius(ctx, matches.nodes[0].key) - : { nodes: [] }; - -console.log({ matches: matches.nodes.length, impact: impact.nodes.length }); +import { createRuntime } from './dist/index.js'; + +const runtime = createRuntime(); +try { + const registration = await runtime.createRegistration({ + repositories: [{ source: { kind: 'local', path: '/absolute/path/to/repository' } }], + }); + const snapshot = await (await runtime.open(registration)).refresh(); + console.log({ workspaceId: snapshot.workspaceId, nodes: snapshot.graph.nodes.length }); +} finally { + await runtime.close(); +} ``` -This describes source-checkout use of current exports, not a production -package compatibility guarantee. See the verified -[RSS programmatic API guide](instructions/RSS-PROGRAMMATIC-API.md). +RSS remains a repository-internal/source-checkout API rather than a package-root +contract. Use the RSS CLI for supported RSS workflows; its internal APIs are +documented separately for repository maintainers. ## Architecture Records and Generated State diff --git a/instructions/RSS-PROGRAMMATIC-API.md b/instructions/RSS-PROGRAMMATIC-API.md index 4c03eaa..fd1ac05 100644 --- a/instructions/RSS-PROGRAMMATIC-API.md +++ b/instructions/RSS-PROGRAMMATIC-API.md @@ -23,9 +23,11 @@ checkout use; the package remains private and unpublished. ### Installation -The RSS API is exported from the built source checkout. After `npm ci` and -`npm run build`, local code may import from `./dist/index.js` or use a local -`npm link`. Do not use `npm install ste-runtime`; the package is not published. +RSS is a repository-internal/source-checkout API. It is not exported from the +`ste-runtime` package root, which exposes the public runtime contract only. +After `npm ci` and `npm run build`, repository-maintainer scripts may import +the built internal module below. Do not use `npm install ste-runtime`; the +package is not published. ```typescript import { @@ -61,13 +63,14 @@ import { type RssQueryResult, type BrokenEdge, type BidirectionalInconsistency, -} from './dist/index.js'; +} from './dist/rss/rss-operations.js'; ``` -For TypeScript tooling, importing directly from the source is also possible: +For source-level TypeScript tooling inside this repository, importing directly +from the internal module is also possible: ```typescript -import { initRssContext, search } from './ste-runtime/src/rss/rss-operations.js'; +import { initRssContext, search } from './src/rss/rss-operations.js'; ``` ### Basic Usage @@ -565,7 +568,8 @@ Complete understanding without misses ### Implementation Pattern ```typescript -import { initRssContext, findEntryPoints, blastRadius } from 'ste-runtime'; +// Repository-internal API; not a package-root import. +import { initRssContext, findEntryPoints, blastRadius } from './dist/rss/rss-operations.js'; async function getRelevantFiles(task: string): Promise { const ctx = await initRssContext('.ste/state'); From d7f6f27f7a85bfe8b9608c67243e7070d2d6df62 Mon Sep 17 00:00:00 2001 From: egallmann Date: Thu, 13 Aug 2026 22:30:53 -0400 Subject: [PATCH 5/6] ci(runtime): centralize contract qualification --- .github/workflows/test.yml | 58 ++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c24f0eb..0afca34 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,17 +44,6 @@ jobs: - name: Run tests run: npm test - - name: Prove architecture compiler (ADR sources only) - run: npm run test:integration - - - name: Verify public-source release policy - run: npm run release:check - - - name: Verify RECON self-documentation - run: | - npm run recon:self - echo "RECON self-documentation completed successfully" - portability: name: OS portability / ${{ matrix.os }} / Node 24 runs-on: ${{ matrix.os }} @@ -90,30 +79,69 @@ jobs: - name: Run unit tests run: npm test - - name: Prove architecture compiler - run: npm run test:integration + contract: + name: Contract qualification / Ubuntu / Node 22 + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: npm + cache-dependency-path: package-lock.json + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies and repository hooks + run: npm ci + + - name: Build + run: npm run build + + - name: Lint + run: npm run lint - name: Run contract guards run: npm run test:contract-guards + - name: Prove architecture compiler + run: npm run test:integration + + - name: Prove public tarball consumer + run: npm run test:public-tarball + - name: Verify public-source release policy run: npm run release:check + - name: Verify RECON self-documentation + run: npm run recon:self + required: name: required if: ${{ always() }} needs: - test - portability + - contract runs-on: ubuntu-latest steps: - name: Verify mandatory test jobs succeeded env: TEST_RESULT: ${{ needs.test.result }} PORTABILITY_RESULT: ${{ needs.portability.result }} + CONTRACT_RESULT: ${{ needs.contract.result }} run: | - if [ "$TEST_RESULT" != "success" ] || [ "$PORTABILITY_RESULT" != "success" ]; then - echo "Required test gate failed: test=$TEST_RESULT portability=$PORTABILITY_RESULT" + if [ "$TEST_RESULT" != "success" ] || [ "$PORTABILITY_RESULT" != "success" ] || [ "$CONTRACT_RESULT" != "success" ]; then + echo "Required test gate failed: test=$TEST_RESULT portability=$PORTABILITY_RESULT contract=$CONTRACT_RESULT" exit 1 fi From 38db9f08f0cb872a81bb473bcd72be54d62af280 Mon Sep 17 00:00:00 2001 From: egallmann Date: Fri, 14 Aug 2026 06:53:18 -0400 Subject: [PATCH 6/6] test(runtime): pin execution adapter failure lifecycle --- .../runtime-workspace-execution.test.ts | 103 +++++++++++++++++- 1 file changed, 101 insertions(+), 2 deletions(-) diff --git a/src/workspace/runtime-workspace-execution.test.ts b/src/workspace/runtime-workspace-execution.test.ts index c1c0312..d767e39 100644 --- a/src/workspace/runtime-workspace-execution.test.ts +++ b/src/workspace/runtime-workspace-execution.test.ts @@ -2,10 +2,23 @@ import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { createRuntime } from '../public/runtime.js'; -import { createWorkspaceExecutionAdapter } from './runtime-workspace-execution.js'; +import { + createWorkspaceExecutionAdapter, + WorkspaceExecutionError, +} from './runtime-workspace-execution.js'; +import { WorkspaceIdentityCollisionError } from './workspace-merge.js'; +import { executeWorkspaceRecon } from './workspace-recon.js'; + +vi.mock('./workspace-recon.js', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + executeWorkspaceRecon: vi.fn(actual.executeWorkspaceRecon), + }; +}); async function createRepository(prefix: string): Promise { const root = await fs.mkdtemp(path.join(os.tmpdir(), `${prefix}-`)); @@ -46,4 +59,90 @@ describe('private runtime workspace execution adapter', () => { await fs.rm(repositoryB, { recursive: true, force: true }); } }); + + it('normalizes execution failures and removes its temporary execution state', async () => { + const repository = await createRepository('runtime-workspace-execution-failure'); + const runtime = createRuntime(); + const adapter = createWorkspaceExecutionAdapter(); + const mockedExecuteWorkspaceRecon = vi.mocked(executeWorkspaceRecon); + let temporaryRoot: string | undefined; + + mockedExecuteWorkspaceRecon.mockImplementationOnce(async options => { + temporaryRoot = path.dirname(options.workspacePath); + throw new Error('deterministic execution failure'); + }); + + try { + const registration = await runtime.createRegistration({ + repositories: [{ source: { kind: 'local', path: repository } }], + }); + + await expect(adapter.execute(registration)).rejects.toMatchObject({ + name: 'WorkspaceExecutionError', + code: 'EXECUTION_FAILED', + message: 'deterministic execution failure', + } satisfies Partial); + expect(temporaryRoot).toBeDefined(); + await expect(fs.access(temporaryRoot!)).rejects.toMatchObject({ code: 'ENOENT' }); + await expect(fs.access(path.join(repository, '.ste'))).rejects.toMatchObject({ code: 'ENOENT' }); + await expect(fs.access(path.join(repository, '.ste-self'))).rejects.toMatchObject({ code: 'ENOENT' }); + await expect(fs.access(path.join(repository, '.workspace-graph'))).rejects.toMatchObject({ code: 'ENOENT' }); + + await adapter.close(); + await adapter.close(); + } finally { + await adapter.close(); + await runtime.close(); + await fs.rm(repository, { recursive: true, force: true }); + } + }); + + it('normalizes identity collisions to public repository IDs and cleans up', async () => { + const repositoryA = await createRepository('runtime-workspace-execution-collision-a'); + const repositoryB = await createRepository('runtime-workspace-execution-collision-b'); + const runtime = createRuntime(); + const adapter = createWorkspaceExecutionAdapter(); + const mockedExecuteWorkspaceRecon = vi.mocked(executeWorkspaceRecon); + let temporaryRoot: string | undefined; + + mockedExecuteWorkspaceRecon.mockImplementationOnce(async options => { + temporaryRoot = path.dirname(options.workspacePath); + throw new WorkspaceIdentityCollisionError([{ + id: 'Service:shared', + repositories: ['repo-1', 'repo-2'], + declarations: [ + { repository: 'repo-1', type: 'Service', name: 'shared' }, + { repository: 'repo-2', type: 'Service', name: 'shared' }, + ], + }]); + }); + + try { + const registration = await runtime.createRegistration({ + repositories: [ + { source: { kind: 'local', path: repositoryA } }, + { source: { kind: 'local', path: repositoryB } }, + ], + }); + const expectedRepositoryIds = [...registration.definition.repositories] + .sort((left, right) => left.repositoryId.localeCompare(right.repositoryId)) + .map(repository => repository.repositoryId); + + await expect(adapter.execute(registration)).rejects.toMatchObject({ + name: 'WorkspaceExecutionError', + code: 'ENTITY_ID_COLLISION', + collisions: [{ id: 'Service:shared', repositoryIds: expectedRepositoryIds }], + } satisfies Partial); + expect(temporaryRoot).toBeDefined(); + await expect(fs.access(temporaryRoot!)).rejects.toMatchObject({ code: 'ENOENT' }); + + await adapter.close(); + await adapter.close(); + } finally { + await adapter.close(); + await runtime.close(); + await fs.rm(repositoryA, { recursive: true, force: true }); + await fs.rm(repositoryB, { recursive: true, force: true }); + } + }); });