Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/project-manifest-bundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@croco/cli": patch
"@croco/protocols-core": patch
"create-croco-app": patch
"@croco/openapi-spec": patch
"@croco/rpc-codegen": patch
---

Generated contract workflows now emit a schema-versioned `.croco/manifest` bundle, validate it through the Project Map drift gate and `croco doctor`, and reference it from generated OpenAPI and RPC outputs.
6 changes: 3 additions & 3 deletions docs/problem-code-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -7556,7 +7556,7 @@
"sources": [
{
"file": "packages/openapi-spec/src/libs/emitOpenAPI.ts",
"line": 92,
"line": 104,
"column": 5,
"kind": "problem-constructor"
}
Expand Down Expand Up @@ -8786,7 +8786,7 @@
"sources": [
{
"file": "packages/rpc-codegen/src/libs/generate.ts",
"line": 101,
"line": 104,
"column": 5,
"kind": "problem-constructor"
}
Expand Down Expand Up @@ -8846,7 +8846,7 @@
"sources": [
{
"file": "packages/rpc-codegen/src/libs/generate.ts",
"line": 107,
"line": 110,
"column": 5,
"kind": "problem-constructor"
}
Expand Down
119 changes: 119 additions & 0 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { spawnSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { defineCommand } from "citty";
import { PROJECT_MANIFEST_BUNDLE_ARTIFACTS } from "@croco/protocols-core";
import { WORKSPACE_MAX_DEPTH } from "../libs/constants.js";
import { CLI_DIAGNOSTIC_CODES, CLI_LEGACY_DIAGNOSTIC_CODES } from "../libs/diagnosticCodes.js";
import type { CliDiagnosticCode } from "../libs/diagnosticCodes.js";
import { GLOBAL_OPTIONS } from "./options.js";
import { PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS } from "./projectMap.js";

export type DoctorSeverity = "error" | "warning";
export type DoctorSummary = "healthy" | "issues_detected";
Expand Down Expand Up @@ -123,13 +125,40 @@ const requiredSaasProviderCapabilities = [
"webhookVerification",
] as const;
const defaultContractGraphSnapshotPath = "contract-graph.snapshot.json";
const defaultProjectManifestBundlePath = ".croco/manifest";
const defaultProblemRegistryPath = "docs/problem-code-registry.json";
const defaultProblemCookbookPath =
"packages/docs/src/content/docs/en/reference/problem-recovery-cookbook.md";
const defaultRuntimeCapabilityManifestPath = "croco-runtime-capability.manifest.json";
const legacyRuntimePolicyManifestPath = "croco-runtime-policy.manifest.json";
const defaultDiGraphManifestPath = ".croco/build/di-graph.manifest.json";
const defaultProviderProfileManifestPath = "croco-saas-profile.manifest.json";
const projectManifestBundleFiles = [
{
path: PROJECT_MANIFEST_BUNDLE_ARTIFACTS.contractGraph,
schemaVersion: PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS.contractGraph,
},
{
path: PROJECT_MANIFEST_BUNDLE_ARTIFACTS.problems,
schemaVersion: PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS.problems,
},
{
path: PROJECT_MANIFEST_BUNDLE_ARTIFACTS.diGraph,
schemaVersion: PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS.diGraph,
},
{
path: PROJECT_MANIFEST_BUNDLE_ARTIFACTS.runtime,
schemaVersion: PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS.runtime,
},
{
path: PROJECT_MANIFEST_BUNDLE_ARTIFACTS.policies,
schemaVersion: PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS.policies,
},
{
path: PROJECT_MANIFEST_BUNDLE_ARTIFACTS.providers,
schemaVersion: PROJECT_MANIFEST_BUNDLE_SCHEMA_VERSIONS.providers,
},
] as const;
const problemRegistryCheckTimeoutMs = 30_000;
const commandOutputMaxLength = 500;

Expand Down Expand Up @@ -173,6 +202,7 @@ export function runDoctor(options: RunDoctorOptions = {}): DoctorReport {
workspaceVersionConsistencyCheck(rootDir, workspace.packages),
spinePackageStateCheck(rootDir, workspace.packages),
contractGraphReadinessCheck(rootDir),
projectManifestBundleReadinessCheck(rootDir),
problemRegistryReadinessCheck(rootDir),
runtimeCapabilityManifestCheck(rootDir),
httpSecurityMiddlewareContractCheck(rootDir, workspace.packages),
Expand Down Expand Up @@ -463,6 +493,95 @@ function contractGraphReadinessCheck(rootDir: string): DoctorCheckResult {
};
}

function projectManifestBundleReadinessCheck(rootDir: string): DoctorCheckResult {
const checkId = "project-manifest-bundle";
const bundleDir = join(rootDir, defaultProjectManifestBundlePath);
const rootScripts = readRootScripts(rootDir);
const expectsBundle =
existsSync(bundleDir) ||
Object.values(rootScripts).some((script) => script.includes("--manifest-bundle"));

if (!expectsBundle) {
return {
id: checkId,
title: "Project manifest bundle",
status: "skipped",
diagnostics: [],
note: `${defaultProjectManifestBundlePath} was not found.`,
};
}

if (!existsSync(bundleDir)) {
return {
id: checkId,
title: "Project manifest bundle",
status: "fail",
diagnostics: [
{
code: CLI_DIAGNOSTIC_CODES.projectMapManifestMissing,
legacyCode: CLI_LEGACY_DIAGNOSTIC_CODES.projectMapManifestMissing,
severity: "error",
checkId,
cause: `${defaultProjectManifestBundlePath} is required by project-map scripts but is missing.`,
location: { file: defaultProjectManifestBundlePath },
action:
"Run croco project map --manifest-bundle .croco/manifest and commit the generated bundle when it is part of the drift gate.",
},
],
};
}

const diagnostics = projectManifestBundleFiles.flatMap((artifact): DoctorDiagnostic[] => {
const artifactPath = join(bundleDir, artifact.path);
const artifactRelativePath = toPosixPath(join(defaultProjectManifestBundlePath, artifact.path));

if (!existsSync(artifactPath)) {
return [
{
code: CLI_DIAGNOSTIC_CODES.projectMapManifestMissing,
legacyCode: CLI_LEGACY_DIAGNOSTIC_CODES.projectMapManifestMissing,
severity: "error",
checkId,
cause: `Project manifest bundle artifact ${artifactRelativePath} is missing.`,
location: { file: artifactRelativePath },
action: "Regenerate the bundle with croco project map --manifest-bundle .croco/manifest.",
},
];
}

const manifest = readJsonObject(artifactPath);
if (manifest.kind === "valid" && manifest.value.schemaVersion === artifact.schemaVersion) {
return [];
}

return [
{
code: CLI_DIAGNOSTIC_CODES.projectMapManifestDrift,
legacyCode: CLI_LEGACY_DIAGNOSTIC_CODES.projectMapManifestDrift,
severity: "error",
checkId,
cause:
manifest.kind === "invalid"
? `${artifactRelativePath} could not be parsed: ${manifest.message}`
: `${artifactRelativePath} must be ${artifact.schemaVersion}.`,
location: { file: artifactRelativePath },
action: "Regenerate the bundle with croco project map --manifest-bundle .croco/manifest.",
},
];
});

return {
id: checkId,
title: "Project manifest bundle",
status: diagnostics.length > 0 ? "fail" : "pass",
diagnostics,
note:
diagnostics.length > 0
? `${diagnostics.length} Project manifest bundle issue(s) found.`
: `${projectManifestBundleFiles.length} schema-versioned manifest bundle artifact(s) are readable.`,
};
}

function problemRegistryReadinessCheck(rootDir: string): DoctorCheckResult {
const checkId = "problem-registry-readiness";
const registryPath = join(rootDir, defaultProblemRegistryPath);
Expand Down
Loading
Loading