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
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:
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user