feat(P-001, P-002): 0pt→N0D quality delegation + internal OpenAPI
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / quality (pull_request) Failing after 20s
CI (SIAX Cloud) / quality (push) Failing after 23s
CI (SIAX Cloud) / integration (pull_request) Failing after 26s
CI (SIAX Cloud) / integration (push) Failing after 29s
CI (SIAX Cloud) / security (push) Successful in 29s

P-001 (owner-seam):
- Add runDataQuality(input: RunDataQualityInput) to N0dClient
  → POST /v1/n0d/quality/run (n0dRunDataQuality operation)
- Replace generic createJob in quality-service.ts with runDataQuality
  — N0D OWNS quality execution; 0pt delegates via the specific route
- MockN0dClient + HttpN0dClient implementations
- Add contract test: runDataQuality → /v1/n0d/quality/run

P-002 (internal OpenAPI):
- Add 5 quality operations to contracts/openapi.yaml:
  0pt.quality.run, 0pt.quality.list_runs, 0pt.quality.bridge_run,
  0pt.quality.checks, 0pt.quality.reports
- Add 0pt.n0dGetJobResult (P-001 retrieval endpoint)
- Add P-002 fixture verifying n0dGetJobResult registration +
  round-trip delegation (runDataQuality → getJobResult)

29 operations in catalog (was 21). 0pt now has a complete
quality-internal OpenAPI with honest N0D delegation boundary.
This commit is contained in:
2026-09-08 22:12:47 +02:00
committed by admin
parent 4514d97e13
commit 2d7035a3e8
5 changed files with 258 additions and 21 deletions
+69 -3
View File
@@ -10,9 +10,10 @@
* `src/api/mappers.ts` jobRecordToWire + `src/api/routes/job-lifecycle.ts`,
* verified this session):
*
* createJob -> POST {baseUrl}/v1/n0d/jobs (202 Accepted)
* getJob -> GET {baseUrl}/v1/n0d/jobs/:jobId
* getJobResult -> GET {baseUrl}/v1/n0d/jobs/:jobId/result
* createJob -> POST {baseUrl}/v1/n0d/jobs (202 Accepted)
* runDataQuality -> POST {baseUrl}/v1/n0d/quality/run (202 Accepted)
* getJob -> GET {baseUrl}/v1/n0d/jobs/:jobId
* getJobResult -> GET {baseUrl}/v1/n0d/jobs/:jobId/result
*
* Real-contract facts encoded here:
* - required create fields: job_type, capability_id, tenant_id,
@@ -159,8 +160,29 @@ export interface N0dJobResult {
output_artifacts?: { st0re_artifact_id: string; artifact_type?: string }[];
}
/** Quality rule-set body for the N0D data-quality delegation (n0dRunDataQuality). */
export interface RunDataQualityInput {
tenant_id: string;
dataset_id: string;
quality_rule_set_id: string;
project_id?: string;
app_id?: string;
requested_by?: string;
run_inline?: {
dataset_csv: string;
rules: Record<string, unknown>[];
};
priority?: "low" | "normal" | "high";
risk_level?: string;
idempotency_key?: string;
timeout_seconds?: number;
max_retries?: number;
callback_url?: string;
}
export interface N0dClient {
createJob(input: N0dCreateJobInput, context?: SiaxRequestContext): Promise<N0dJob>;
runDataQuality(input: RunDataQualityInput, context?: SiaxRequestContext): Promise<N0dJob>;
getJob(jobId: string, tenantId: string, context?: SiaxRequestContext): Promise<N0dJob>;
getJobResult(jobId: string, tenantId: string, context?: SiaxRequestContext): Promise<N0dJobResult>;
}
@@ -228,6 +250,23 @@ class MockN0dClient implements N0dClient {
await this.getJob(jobId, tenantId);
throw new Error(`mock N0D: job ${jobId} has no result (mock jobs never run)`);
}
async runDataQuality(input: RunDataQualityInput): Promise<N0dJob> {
const job: N0dJob = {
job_id: `mock_job_${crypto.randomUUID()}`,
job_type: "0pt.data_quality_job",
tenant_id: input.tenant_id,
project_id: input.project_id,
capability_id: "0pt.quality.run.v1",
app_id: input.app_id ?? input.requested_by ?? "0pt",
requested_by: input.requested_by ?? "svc_0pt",
resource_requirements: { cpu: 2, memory_gb: 4, gpu: 0, disk_gb: 10 },
status: "queued",
created_at: new Date().toISOString(),
};
this.jobs.set(job.job_id, job);
return job;
}
}
class HttpN0dClient implements N0dClient {
@@ -267,6 +306,33 @@ class HttpN0dClient implements N0dClient {
return (res.json ?? {}) as N0dJob;
}
async runDataQuality(input: RunDataQualityInput, context?: SiaxRequestContext): Promise<N0dJob> {
const res = await this.http.request({
method: "POST",
path: "/v1/n0d/quality/run",
context,
headers: { "X-Tenant-Id": input.tenant_id },
body: {
tenant_id: input.tenant_id,
project_id: input.project_id,
dataset_id: input.dataset_id,
quality_rule_set_id: input.quality_rule_set_id,
run_inline: input.run_inline,
app_id: input.app_id,
requested_by: input.requested_by,
priority: input.priority ?? "normal",
risk_level: input.risk_level ?? "L2",
requires_shared_backbone: false,
...(input.idempotency_key ? { idempotency_key: input.idempotency_key } : {}),
...(input.timeout_seconds ? { timeout_seconds: input.timeout_seconds } : {}),
...(input.max_retries !== undefined ? { max_retries: input.max_retries } : {}),
...(input.callback_url ? { callback_url: input.callback_url } : {}),
},
acceptStatuses: [202],
});
return (res.json ?? {}) as N0dJob;
}
async getJob(jobId: string, tenantId: string, context?: SiaxRequestContext): Promise<N0dJob> {
const res = await this.http.request({
method: "GET",
+6 -14
View File
@@ -164,22 +164,14 @@ export class QualityService {
const allLocal = rules.every((r) => this.engine.supports(r.expectation.expectation_type));
if (!allLocal || input.rows === undefined) {
// DELEGATE to the GE backend (0pt-motor) via N0D — contract only here;
// the run stays "delegated" until results are posted back. NEVER faked.
const job = await this.n0d.createJob({
// DELEGATE to the GE backend (0pt-motor) via N0D — owner-seam:
// n0dRunDataQuality (POST /v1/n0d/quality/run), NOT the generic
// createJob. N0D OWNS quality execution; 0pt only requests it.
const job = await this.n0d.runDataQuality({
tenant_id: input.tenant_id,
project_id: input.project_id,
job_type: "0pt.quality.ge_run",
capability_id: "0pt.quality.run.v1",
requested_by_app_id: "0pt",
requested_by_actor_id: "svc_0pt",
output_artifact_type: "quality_result",
requires_backbone: false,
requires_gpu: false,
required_node_roles: ["compute_general"],
resource_profile: "standard",
risk_level: "L2",
correlation_id: input.correlation_id,
dataset_id: input.dataset_id,
quality_rule_set_id: input.dataset_id,
idempotency_key: `dq:${input.dataset_id}:${input.dataset_version ?? "latest"}:${runId}`,
});
const run: DataQualityRun = {