Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Opcore is the code-intelligence and robustness monorepo for graph context, edit
- #16 Python generated/private/dependency roots are excluded from discovery and status census: `.venv`, `venv`, `env`, `__pycache__`, `.eggs`, `build`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, `site-packages`, `*.egg-info`, and `*.dist-info` - WHY: dependency/cache artifacts must not create graph freshness or coverage evidence.
- #17 Python export metadata is best-effort: `__all__` wins when present, otherwise the leading-underscore convention marks module-level public names; file `exports[]` entries must include policy and supportedSymbol - WHY: Python has no enforced export boundary.
- #244 `python.syntax` resolves one concrete interpreter through the Python project resolver, executes that exact interpreter with an isolated versioned JSON compile protocol over sorted `.py`/`.pyi` after-state content, and reports ranged/provenanced compiler diagnostics plus fine-grained outcomes. Parser-success overrides and hand-written grammar heuristics are forbidden; malformed output, nonzero exits, signals, and timeouts fail closed - WHY: syntax truth, target-version truth, and tool provenance must come from the same compiler invocation without mutating the source repo.
- #246 makes `@the-open-engine/opcore-validation-python` the sole dynamic owner of `opcore.python.project-context.v1`: every Python target resolves against its nearest project boundary through an injected read/list/exists/realpath workspace view, smol-toml AST config/build metadata, exact interpreter/tool/build probes, and after-state content. Missing realpath evidence is ambiguous, deleted overlays cannot remain discovery markers, and declared constraints never become invented exact versions. Validation, status, scan, init/install preview, metrics, ASP, and installed execution must reuse the resulting project key, context fingerprint, outcome, and provenance; ASP workspace/config reads must remain host-callback-only. Static descriptors advertise only the schema, outcome vocabulary, read-only behavior, and no-install guarantee - WHY: root-scoped and duplicate project/environment discovery validates nested monorepo files with the wrong interpreter and makes surfaces disagree.
- #19 requires graph status to preserve real WAL checkpoint evidence from the latest pipeline summary and release gates to fail missing/fabricated WAL evidence - WHY: freshness and checkpoint pressure must remain host-visible provider facts.
- #19 treats `opcore graph serve` as the stdio/MCP hot-query replacement, not a Unix socket, with parallel independent serve sessions as the supported concurrency evidence.
- #19 keeps current external CRG receipts as non-implementation compatibility evidence only - WHY: old CRG remains a guardrail until downstream cutover issues consume the Opcore proof.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ Opcore is the code-intelligence and robustness monorepo for graph context, edit
- #16 Python generated/private/dependency roots are excluded from discovery and status census: `.venv`, `venv`, `env`, `__pycache__`, `.eggs`, `build`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.ruff_cache`, `site-packages`, `*.egg-info`, and `*.dist-info` - WHY: dependency/cache artifacts must not create graph freshness or coverage evidence.
- #17 Python export metadata is best-effort: `__all__` wins when present, otherwise the leading-underscore convention marks module-level public names; file `exports[]` entries must include policy and supportedSymbol - WHY: Python has no enforced export boundary.
- #244 `python.syntax` resolves one concrete interpreter through the Python project resolver, executes that exact interpreter with an isolated versioned JSON compile protocol over sorted `.py`/`.pyi` after-state content, and reports ranged/provenanced compiler diagnostics plus fine-grained outcomes. Parser-success overrides and hand-written grammar heuristics are forbidden; malformed output, nonzero exits, signals, and timeouts fail closed - WHY: syntax truth, target-version truth, and tool provenance must come from the same compiler invocation without mutating the source repo.
- #246 makes `@the-open-engine/opcore-validation-python` the sole dynamic owner of `opcore.python.project-context.v1`: every Python target resolves against its nearest project boundary through an injected read/list/exists/realpath workspace view, smol-toml AST config/build metadata, exact interpreter/tool/build probes, and after-state content. Missing realpath evidence is ambiguous, deleted overlays cannot remain discovery markers, and declared constraints never become invented exact versions. Validation, status, scan, init/install preview, metrics, ASP, and installed execution must reuse the resulting project key, context fingerprint, outcome, and provenance; ASP workspace/config reads must remain host-callback-only. Static descriptors advertise only the schema, outcome vocabulary, read-only behavior, and no-install guarantee - WHY: root-scoped and duplicate project/environment discovery validates nested monorepo files with the wrong interpreter and makes surfaces disagree.
- #19 requires graph status to preserve real WAL checkpoint evidence from the latest pipeline summary and release gates to fail missing/fabricated WAL evidence - WHY: freshness and checkpoint pressure must remain host-visible provider facts.
- #19 treats `opcore graph serve` as the stdio/MCP hot-query replacement, not a Unix socket, with parallel independent serve sessions as the supported concurrency evidence.
- #19 keeps current external CRG receipts as non-implementation compatibility evidence only - WHY: old CRG remains a guardrail until downstream cutover issues consume the Opcore proof.
Expand Down
30 changes: 29 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"lint": "node scripts/check-workspace.mjs",
"pack:check": "node scripts/check-packages.mjs",
"provenance:check": "node scripts/check-provenance.mjs",
"python:resolver-matrix": "node scripts/check-python-resolver-matrix.mjs",
"release:hygiene": "node scripts/check-release-hygiene.mjs",
"rust:check": "npm run rust:fmt && npm run rust:clippy && npm run rust:test",
"rust:clippy": "cargo clippy --all-targets --all-features -- -D warnings",
Expand Down
12 changes: 12 additions & 0 deletions packages/asp-provider/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export type OpcoreAspProviderManifest = {
protocolVersion: string;
capabilityFamilies: readonly ["check"];
checks: readonly string[];
pythonProjectContext: {
schemaId: "opcore.python.project-context.v1";
outcomes: readonly ["resolved", "degraded", "unsupported", "ambiguous"];
readOnly: true;
installs: false;
};
executable: {
packageName: string;
bin: "opcore-asp-provider";
Expand Down Expand Up @@ -102,6 +108,12 @@ export function createOpcoreAspProviderManifest(options: { packageRoot?: string;
protocolVersion: ASP_PROTOCOL_VERSION,
capabilityFamilies: ["check"],
checks: defaultAspProviderValidationCheckIds,
pythonProjectContext: {
schemaId: "opcore.python.project-context.v1",
outcomes: ["resolved", "degraded", "unsupported", "ambiguous"],
readOnly: true,
installs: false
},
executable: {
packageName: OPCORE_PROVIDER_PACKAGE,
bin: "opcore-asp-provider",
Expand Down
65 changes: 62 additions & 3 deletions packages/asp-provider/src/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ export function initializeResult(params: InitializeParams): JsonObject {
manifest: defaultAspProviderValidationManifest,
scopes: ["changeset", "paths"],
comparisons: [supportedComparison],
fixes: false
fixes: false,
pythonProjectContext: {
schemaId: "opcore.python.project-context.v1",
outcomes: ["resolved", "degraded", "unsupported", "ambiguous"],
readOnly: true,
installs: false
}
}
},
requestedPermissions: {
Expand Down Expand Up @@ -119,7 +125,7 @@ export async function evaluateChangeset(
};
});
validationResult = await timing.timeAsync("validation", () =>
createAspProviderValidationRunner(workspace.workspace).runValidation(request)
createAspProviderValidationRunner(workspace.workspace, workspace.pythonWorkspace).runValidation(request)
);
} catch (error) {
const validationFailure = errorAssessment({
Expand Down Expand Up @@ -429,6 +435,19 @@ function validationCoverageDegradations(result: ValidationResult, selectedIds: r
} {
const degraded: ProviderCoverageDegradation[] = [];
const unsupported: ProviderCoverageDegradation[] = [];
for (const context of result.pythonProjectContexts ?? []) {
if (context.outcome === "resolved") continue;
const reason = context.outcome === "unsupported" ? "unsupported" : "unavailable";
const entry = {
source: providerSource,
reason,
requirement: `python-project-context:${context.target}`,
detail: context.reasons.map((item) => `${item.code}: ${item.message}`).join("; ") ||
`Python project context ${context.outcome}`
};
degraded.push(entry);
if (reason === "unsupported") unsupported.push(entry);
}
for (const skipped of result.manifest?.skippedChecks ?? []) {
const entry = {
source: providerSource,
Expand Down Expand Up @@ -538,13 +557,53 @@ function validationEvidence(result: ValidationResult): JsonObject[] {
data.refusalCategory = result.refusal.category;
data.refusalMessage = result.refusal.message;
}
return [
const evidence: JsonObject[] = [
{
kind: "message",
message: "Opcore validation result mapped to ASP Core check assessment.",
data
}
];
for (const context of result.pythonProjectContexts ?? []) {
evidence.push({
kind: "python_project_context",
message: `Canonical Python project context for ${context.target}.`,
data: {
schemaId: context.schemaId,
target: context.target,
projectRoot: context.projectRoot,
projectKey: context.projectKey,
contextFingerprint: context.contextFingerprint,
outcome: context.outcome,
interpreter: context.interpreter === undefined
? null
: {
executable: context.interpreter.executable,
argv: [...context.interpreter.argv],
cwd: context.interpreter.cwd,
source: context.interpreter.source,
...(context.interpreter.version === undefined ? {} : { version: context.interpreter.version }),
...(context.interpreter.implementation === undefined ? {} : { implementation: context.interpreter.implementation }),
...(context.interpreter.platform === undefined ? {} : { platform: context.interpreter.platform }),
...(context.interpreter.architecture === undefined ? {} : { architecture: context.interpreter.architecture }),
...(context.interpreter.abi === undefined ? {} : { abi: context.interpreter.abi }),
...(context.interpreter.soabi === undefined ? {} : { soabi: context.interpreter.soabi })
},
tools: context.tools.map((tool) => ({
tool: tool.tool,
available: tool.available,
executable: tool.executable,
argv: [...tool.argv],
cwd: tool.cwd,
source: tool.source,
...(tool.configFile === undefined ? {} : { configFile: tool.configFile }),
...(tool.version === undefined ? {} : { version: tool.version })
})),
reasons: context.reasons.map((reason) => ({ code: reason.code, message: reason.message }))
}
});
}
return evidence;
}

function providerMetadata(): Assessment["provider"] {
Expand Down
20 changes: 12 additions & 8 deletions packages/asp-provider/src/validation-composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from "@the-open-engine/opcore-validation";
import {
createBuiltInValidationChecks,
validationChecksForRepoPolicy
parseOpcoreRepoConfig
} from "@the-open-engine/opcore-validation-policy";
import {
graphProviderDetectChanges,
Expand All @@ -39,6 +39,7 @@ import {
graphProviderReviewContext,
graphProviderStatus
} from "@the-open-engine/opcore-graph";
import type { PythonProjectWorkspace } from "@the-open-engine/opcore-validation-python";

const aspProviderPolicyOptions = { clone: false } as const;

Expand All @@ -47,14 +48,15 @@ export const defaultAspProviderValidationChecks = createBuiltInValidationChecks(
export const defaultAspProviderValidationCheckIds = defaultAspProviderValidationChecks.map((check) => check.id);
export const defaultAspProviderValidationManifest = createValidationCheckManifest(defaultAspProviderValidationChecks);

export function createAspProviderValidationRunner(workspace: ValidationWorkspace): {
export function createAspProviderValidationRunner(workspace: ValidationWorkspace, pythonWorkspace: PythonProjectWorkspace): {
runValidation(request: ValidationRequest): Promise<ValidationResult>;
} {
return {
runValidation(request) {
async runValidation(request) {
const config = await readHostConfig(workspace, request);
return createValidationRunner({
workspace,
checks: validationChecksForAspRequest(request),
checks: createBuiltInValidationChecks(config, { ...aspProviderPolicyOptions, pythonWorkspace }),
graphProviderClient: createAspValidationGraphProviderClient()
}).runValidation(request);
}
Expand All @@ -67,10 +69,12 @@ export function selectedValidationChecks(checkIds?: readonly string[]): readonly
return defaultAspProviderValidationChecks.filter((check) => requested.has(check.id));
}

function validationChecksForAspRequest(request: ValidationRequest): readonly ValidationCheckDefinition[] {
const repoRoot = request.repo.repoRoot;
if (repoRoot === undefined) return defaultAspProviderValidationChecks;
return validationChecksForRepoPolicy(repoRoot, aspProviderPolicyOptions);
async function readHostConfig(workspace: ValidationWorkspace, request: ValidationRequest) {
const overlay = request.overlays.find((entry) => entry.path === ".opcore/config");
if (overlay?.action === "write") return parseOpcoreRepoConfig(overlay.content);
if (overlay?.action === "delete") return parseOpcoreRepoConfig(undefined);
const result = await workspace.readFile(".opcore/config");
return parseOpcoreRepoConfig(result.status === "found" ? result.content : undefined);
}

function createAspValidationGraphProviderClient(): ValidationGraphProviderClient {
Expand Down
25 changes: 24 additions & 1 deletion packages/asp-provider/src/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { ValidationWorkspace, ValidationWorkspaceFileSet, ValidationWorkspaceReadFileResult } from "@the-open-engine/opcore-validation";
import { calculateValidationFileChecksum } from "@the-open-engine/opcore-validation";
import type { PythonProjectWorkspace } from "@the-open-engine/opcore-validation-python";
import type { JsonRpcPeer } from "./json-rpc.js";
import type { Baseline, ChangeSet, EncodedBlob, InitializedParams, InitializeParams, InlineBlob, JsonObject } from "./protocol.js";

export type AspHostValidationWorkspace = {
workspace: ValidationWorkspace;
pythonWorkspace: PythonProjectWorkspace;
readBlobText(blobId: string): Promise<string>;
readInlineOrBlobText(blob: string | InlineBlob | undefined, label: string): Promise<string>;
checksumForBlob(blobId: string): Promise<string>;
Expand Down Expand Up @@ -76,6 +78,7 @@ export function createAspHostValidationWorkspace(

const workspace: ValidationWorkspace = {
readFile,
listFiles: async () => listRepoFiles(),
listRepoFiles,
listPackageFiles: async (_packageName, packageRoot) => {
const files = await listRepoFiles();
Expand All @@ -88,8 +91,25 @@ export function createAspHostValidationWorkspace(
}
};

const pythonWorkspace: PythonProjectWorkspace = {
read: async (path) => {
const result = await readFile(path);
return result.status === "found" ? result.content : undefined;
},
list: async () => (await listTree()).filter(isFileLikeTreeEntry).map((entry) => entry.path).sort(),
exists: async (path) => (await readFile(path)).status === "found",
realpath: async (path) => {
let entry = entryByPath.get(path);
if (entry === undefined) entry = (await listTree([path])).find((candidate) => candidate.path === path);
if (entry === undefined || entry.kind === undefined) return { path, symlink: false, unavailable: true };
return entry.kind === "file" ? { path, symlink: false } : { path, symlink: true };
},
executableExists: async () => false
};

return {
workspace,
pythonWorkspace,
readBlobText,
readInlineOrBlobText: async (blob, label) => {
if (typeof blob === "string") return readBlobText(blob);
Expand All @@ -110,14 +130,17 @@ function normalizeTreeEntry(value: unknown): TreeEntry | undefined {
if (!value || typeof value !== "object") return undefined;
const entry = value as { path?: unknown; blobId?: unknown; kind?: unknown };
if (typeof entry.path !== "string" || typeof entry.blobId !== "string") return undefined;
if (entry.kind !== undefined && entry.kind !== "file") return undefined;
return {
path: entry.path,
blobId: entry.blobId,
kind: typeof entry.kind === "string" ? entry.kind : undefined
};
}

function isFileLikeTreeEntry(entry: TreeEntry): boolean {
return entry.kind === undefined || entry.kind === "file" || entry.kind === "symlink";
}

function decodeBlob(blob: EncodedBlob): string {
if (typeof blob.content === "string") return blob.content;
if (typeof blob.bytes !== "string") throw new Error(`Host blob ${blob.id} is missing bytes`);
Expand Down
Loading
Loading