From dcf8db1055ff365ea10005f57f33200a8c87d18c Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:27:44 -0300 Subject: [PATCH 01/12] feat(conformance): add decision-runtime.ts --- src/application/ports/decision-runtime.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/application/ports/decision-runtime.ts diff --git a/src/application/ports/decision-runtime.ts b/src/application/ports/decision-runtime.ts new file mode 100644 index 0000000..0e579ed --- /dev/null +++ b/src/application/ports/decision-runtime.ts @@ -0,0 +1,17 @@ +import type { DecisionSpec, Scalar } from '../../domain/decisions.js'; + +export interface DecisionRuntimeRequest { + decisionId: string; + spec: DecisionSpec; + input: Record; +} + +export interface DecisionRuntimeResult { + value: Scalar; + ruleId: string; +} + +export interface DecisionRuntime { + readonly name: string; + evaluate(request: DecisionRuntimeRequest): Promise; +} From 30ce26c50fc57cc4bf13753b520f5f3ebef6bacb Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:27:47 -0300 Subject: [PATCH 02/12] feat(conformance): add decision-conformance.ts --- src/application/decision-conformance.ts | 75 +++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/application/decision-conformance.ts diff --git a/src/application/decision-conformance.ts b/src/application/decision-conformance.ts new file mode 100644 index 0000000..b765552 --- /dev/null +++ b/src/application/decision-conformance.ts @@ -0,0 +1,75 @@ +import { evaluateDecision } from '../domain/decisions.js'; +import type { DecisionSpec, Scalar } from '../domain/decisions.js'; +import type { DecisionRuntime, DecisionRuntimeResult } from './ports/decision-runtime.js'; + +export interface DecisionConformanceCase { + id: string; + scenario?: string; + input: Record; + expected: Scalar; + reference?: DecisionRuntimeResult; + runtime?: DecisionRuntimeResult; + passed: boolean; + error?: string; +} + +export interface DecisionConformanceResult { + decision: string; + runtime: string; + passed: boolean; + cases: DecisionConformanceCase[]; + matched: number; + divergent: number; +} + +export async function conformDecision( + decisionId: string, + spec: DecisionSpec, + runtime: DecisionRuntime, +): Promise { + const cases: DecisionConformanceCase[] = []; + + for (const testCase of spec.cases) { + try { + const reference = evaluateDecision(spec, testCase.input); + const candidate = await runtime.evaluate({ + decisionId, + spec, + input: testCase.input, + }); + const passed = + Object.is(reference.value, testCase.expected) && + Object.is(candidate.value, reference.value) && + candidate.ruleId === reference.ruleId; + + cases.push({ + id: testCase.id, + scenario: testCase.scenario, + input: testCase.input, + expected: testCase.expected, + reference, + runtime: candidate, + passed, + }); + } catch (error) { + cases.push({ + id: testCase.id, + scenario: testCase.scenario, + input: testCase.input, + expected: testCase.expected, + passed: false, + error: (error as Error).message, + }); + } + } + + const matched = cases.filter(c => c.passed).length; + return { + decision: decisionId, + runtime: runtime.name, + passed: cases.length > 0 && matched === cases.length, + cases, + matched, + divergent: cases.length - matched, + }; +} From 59d74dfe52f0facb0c32f4de3e52df84ce3c9a72 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:27:50 -0300 Subject: [PATCH 03/12] feat(conformance): add reference-decision-runtime.ts --- .../reference/reference-decision-runtime.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/infrastructure/reference/reference-decision-runtime.ts diff --git a/src/infrastructure/reference/reference-decision-runtime.ts b/src/infrastructure/reference/reference-decision-runtime.ts new file mode 100644 index 0000000..aa02c34 --- /dev/null +++ b/src/infrastructure/reference/reference-decision-runtime.ts @@ -0,0 +1,10 @@ +import { evaluateDecision } from '../../domain/decisions.js'; +import type { DecisionRuntime, DecisionRuntimeRequest } from '../../application/ports/decision-runtime.js'; + +export class ReferenceDecisionRuntime implements DecisionRuntime { + readonly name = 'reference'; + + async evaluate(request: DecisionRuntimeRequest) { + return evaluateDecision(request.spec, request.input); + } +} From b2d76db82a8881d69f7ecc8db9692c7d9c93acd6 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:27:52 -0300 Subject: [PATCH 04/12] feat(conformance): add rego-generator.ts --- src/infrastructure/opa/rego-generator.ts | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/infrastructure/opa/rego-generator.ts diff --git a/src/infrastructure/opa/rego-generator.ts b/src/infrastructure/opa/rego-generator.ts new file mode 100644 index 0000000..4da220b --- /dev/null +++ b/src/infrastructure/opa/rego-generator.ts @@ -0,0 +1,32 @@ +import type { DecisionSpec, Scalar } from '../../domain/decisions.js'; + +function scalar(value: Scalar): string { + return JSON.stringify(value); +} + +export function opaPackageName(decisionId: string): string { + const safe = decisionId.toLowerCase().replace(/[^a-z0-9_]/g, '_'); + return `prd.decision.${safe}`; +} + +export function generateDecisionRego(decisionId: string, spec: DecisionSpec): string { + const packageName = opaPackageName(decisionId); + const rules = spec.rules.map(rule => { + const conditions = Object.entries(rule.when) + .map(([name, value]) => ` input[${JSON.stringify(name)}] == ${scalar(value)}`) + .join('\n'); + const body = conditions || ' true'; + return `matches contains {"value": ${scalar(rule.then)}, "ruleId": ${JSON.stringify(rule.id)}} if {\n${body}\n}`; + }); + + return [ + `package ${packageName}`, + '', + ...rules.flatMap(rule => [rule, '']), + 'result := item if {', + ' count(matches) == 1', + ' item := matches[_]', + '}', + '', + ].join('\n'); +} From d65fc58ffb76c199ec3ca33282c48f231a7f9378 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:27:54 -0300 Subject: [PATCH 05/12] feat(conformance): add opa-decision-runtime.ts --- .../opa/opa-decision-runtime.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/infrastructure/opa/opa-decision-runtime.ts diff --git a/src/infrastructure/opa/opa-decision-runtime.ts b/src/infrastructure/opa/opa-decision-runtime.ts new file mode 100644 index 0000000..c87b6c0 --- /dev/null +++ b/src/infrastructure/opa/opa-decision-runtime.ts @@ -0,0 +1,97 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { IntentError, isRecord } from '../../domain/model.js'; +import type { Scalar } from '../../domain/decisions.js'; +import type { + DecisionRuntime, + DecisionRuntimeRequest, + DecisionRuntimeResult, +} from '../../application/ports/decision-runtime.js'; +import { generateDecisionRego, opaPackageName } from './rego-generator.js'; + +function isScalar(value: unknown): value is Scalar { + return typeof value === 'string' || typeof value === 'boolean' || + (typeof value === 'number' && Number.isFinite(value)); +} + +export class OpaDecisionRuntime implements DecisionRuntime { + readonly name = 'opa'; + + constructor(readonly binary = process.env.PRD_OPA_BINARY || 'opa') {} + + async evaluate(request: DecisionRuntimeRequest): Promise { + const dir = mkdtempSync(join(tmpdir(), 'prd-opa-')); + const policy = join(dir, 'decision.rego'); + + try { + writeFileSync(policy, generateDecisionRego(request.decisionId, request.spec), 'utf8'); + const query = `data.${opaPackageName(request.decisionId)}.result`; + const result = spawnSync( + this.binary, + [ + 'eval', + '--format=json', + '--strict', + '--fail', + '--stdin-input', + '--data', + policy, + query, + ], + { + input: JSON.stringify(request.input), + encoding: 'utf8', + windowsHide: true, + }, + ); + + if (result.error) { + const code = (result.error as NodeJS.ErrnoException).code; + if (code === 'ENOENT') { + throw new IntentError( + 'OPA_RUNTIME_UNAVAILABLE', + `OPA executable not found: ${this.binary}. Install OPA or set PRD_OPA_BINARY.`, + ); + } + throw result.error; + } + + if (result.status !== 0) { + throw new IntentError( + 'OPA_EVAL_FAILED', + (result.stderr || result.stdout || 'OPA evaluation failed.').trim(), + ); + } + + let document: unknown; + try { + document = JSON.parse(result.stdout); + } catch { + throw new IntentError('OPA_INVALID_OUTPUT', 'OPA returned invalid JSON.'); + } + + if (!isRecord(document) || !Array.isArray(document.result)) { + throw new IntentError('OPA_INVALID_OUTPUT', 'OPA result array is missing.'); + } + const first = document.result[0]; + if (!isRecord(first) || !Array.isArray(first.expressions)) { + throw new IntentError('OPA_INVALID_OUTPUT', 'OPA expressions are missing.'); + } + const expression = first.expressions[0]; + if (!isRecord(expression) || !isRecord(expression.value)) { + throw new IntentError('OPA_INVALID_OUTPUT', 'OPA decision result is missing.'); + } + + const value = expression.value.value; + const ruleId = expression.value.ruleId; + if (!isScalar(value) || typeof ruleId !== 'string') { + throw new IntentError('OPA_INVALID_OUTPUT', 'OPA decision result has invalid value or ruleId.'); + } + return { value, ruleId }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } + } +} From 649f96fe731fef0e6b056d6f25229ff392746c42 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:28:14 -0300 Subject: [PATCH 06/12] feat(conformance): add decision runtime CLI --- src/interfaces/cli/main.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/interfaces/cli/main.ts b/src/interfaces/cli/main.ts index f8f3adb..e33364d 100644 --- a/src/interfaces/cli/main.ts +++ b/src/interfaces/cli/main.ts @@ -11,6 +11,9 @@ import { graph, mermaid } from '../../domain/graph.js'; import { IntentError } from '../../domain/model.js'; import { evaluateDecision, analyzeDecision } from '../../domain/decisions.js'; import type { DecisionSpec } from '../../domain/decisions.js'; +import { conformDecision } from '../../application/decision-conformance.js'; +import { ReferenceDecisionRuntime } from '../../infrastructure/reference/reference-decision-runtime.js'; +import { OpaDecisionRuntime } from '../../infrastructure/opa/opa-decision-runtime.js'; const cli = new Command().name('prd').description('PRD as a Code').version('0.2.0-alpha.1') .option('-C, --root ', 'product directory or a child directory', '.') .option('--json', 'machine-readable output').showHelpAfterError().showSuggestionAfterError().exitOverride(); @@ -43,6 +46,16 @@ cli.command('test').description('Execute typed decision cases; report unbound Gh const decision = cli.command('decision').description('Evaluate supported decision tables'); decision.command('evaluate').argument('').requiredOption('--input ', 'typed input object').action((id, opts) => { const a = service().load().artifacts.find(s => s.artifact.metadata.id === id && s.artifact.kind === 'Decision'); if (!a) throw new IntentError('UNKNOWN_DECISION', id); out(evaluateDecision(a.artifact.spec as unknown as DecisionSpec, JSON.parse(opts.input))); }); decision.command('analyze').argument('').action(id => { const a = service().load().artifacts.find(s => s.artifact.metadata.id === id && s.artifact.kind === 'Decision'); if (!a) throw new IntentError('UNKNOWN_DECISION', id); const result = analyzeDecision(a.artifact.spec as unknown as DecisionSpec); out(result); if (result.status !== 'valid') process.exitCode = 1; }); +const conformance = cli.command('conformance').description('Compare executable decision runtimes with PRD as a Code reference semantics'); +conformance.command('decision').argument('').addOption(new Option('--runtime ').choices(['reference', 'opa']).default('reference')) + .action(async (id, opts) => { + const a = service().load().artifacts.find(s => s.artifact.metadata.id === id && s.artifact.kind === 'Decision'); + if (!a) throw new IntentError('UNKNOWN_DECISION', id); + const runtime = opts.runtime === 'opa' ? new OpaDecisionRuntime() : new ReferenceDecisionRuntime(); + const result = await conformDecision(id, a.artifact.spec as unknown as DecisionSpec, runtime); + out(result, `${result.passed ? '✓' : '✗'} ${id} on ${result.runtime}: ${result.matched}/${result.cases.length} cases conform`); + if (!result.passed) process.exitCode = 1; + }); cli.command('cite').argument('').option('--into ', 'append to supporting document').action((id, opts) => { const s = service(); if (opts.into) out(s.citeInto(id, opts.into)); else { const block = s.cite(id); out({ block }, block.trimEnd()); } }); const citations = cli.command('citations').description('Verify current/stale/tampered/unresolved native citations'); citations.command('check').action(() => { const r = service().citations(); out(r); if (!r.valid) process.exitCode = 1; }); From a653770a27bc292d0184bda481f26f5ad2a2d964 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:28:17 -0300 Subject: [PATCH 07/12] feat(conformance): export decision runtime APIs --- src/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/index.ts b/src/index.ts index 76f73be..f5127ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,3 +7,8 @@ export * from './application/product-service.js'; export { createCompiler } from './compiler/compile.js'; export { createWorkspace } from './infrastructure/filesystem/workspace.js'; export { services } from './bootstrap.js'; +export * from './application/ports/decision-runtime.js'; +export * from './application/decision-conformance.js'; +export * from './infrastructure/reference/reference-decision-runtime.js'; +export * from './infrastructure/opa/rego-generator.js'; +export * from './infrastructure/opa/opa-decision-runtime.js'; From 25c0b016508160cf1b241d081a6e82e9846eb5e7 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:28:37 -0300 Subject: [PATCH 08/12] test(conformance): cover reference and Rego adapter --- tests/conformance.test.mjs | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/conformance.test.mjs diff --git a/tests/conformance.test.mjs b/tests/conformance.test.mjs new file mode 100644 index 0000000..10f101e --- /dev/null +++ b/tests/conformance.test.mjs @@ -0,0 +1,62 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { conformDecision } from '../dist/application/decision-conformance.js'; +import { ReferenceDecisionRuntime } from '../dist/infrastructure/reference/reference-decision-runtime.js'; +import { generateDecisionRego, opaPackageName } from '../dist/infrastructure/opa/rego-generator.js'; + +const spec = { + profile: 'dmn-table/v1', + hitPolicy: 'UNIQUE', + inputs: [ + { name: 'active', type: 'boolean' }, + { name: 'funded', type: 'boolean' }, + ], + output: { + name: 'result', + type: 'string', + values: ['ALLOW', 'DENY'], + }, + rules: [ + { id: 'allow', when: { active: true, funded: true }, then: 'ALLOW' }, + { id: 'inactive', when: { active: false }, then: 'DENY' }, + { id: 'unfunded', when: { active: true, funded: false }, then: 'DENY' }, + ], + cases: [ + { id: 'C-1', input: { active: true, funded: true }, expected: 'ALLOW' }, + { id: 'C-2', input: { active: false, funded: true }, expected: 'DENY' }, + { id: 'C-3', input: { active: false, funded: false }, expected: 'DENY' }, + { id: 'C-4', input: { active: true, funded: false }, expected: 'DENY' }, + ], +}; + +test('reference runtime conforms to current decision semantics', async () => { + const result = await conformDecision('DEC-TEST', spec, new ReferenceDecisionRuntime()); + assert.equal(result.passed, true); + assert.equal(result.matched, 4); + assert.equal(result.divergent, 0); +}); + +test('conformance detects a divergent candidate runtime', async () => { + const runtime = { + name: 'broken', + async evaluate(request) { + const reference = await new ReferenceDecisionRuntime().evaluate(request); + return request.input.active === true && request.input.funded === true + ? { value: 'DENY', ruleId: reference.ruleId } + : reference; + }, + }; + const result = await conformDecision('DEC-TEST', spec, runtime); + assert.equal(result.passed, false); + assert.equal(result.divergent, 1); + assert.equal(result.cases.find(c => c.id === 'C-1').passed, false); +}); + +test('rego generator preserves decision ids, values and wildcard rows', () => { + const rego = generateDecisionRego('DEC-TEST', spec); + assert.equal(opaPackageName('DEC-TEST'), 'prd.decision.dec_test'); + assert.match(rego, /package prd\.decision\.dec_test/); + assert.match(rego, /input\["active"\] == true/); + assert.match(rego, /"ruleId": "allow"/); + assert.match(rego, /count\(matches\) == 1/); +}); From f84698045ad9e1eaff95d930636e72182c927bbc Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:29:06 -0300 Subject: [PATCH 09/12] docs(conformance): document decision runtime command --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 82764aa..3d9d5a7 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,8 @@ npm exec --no -- prd validate --strict - `init` com agente, instruções, cinco templates de skills e workflow de CI para Copilot. - Migração aditiva do nosso protótipo anterior e CLI legado isolado. +O runtime `reference` usa a semântica nativa de `evaluateDecision`; `--runtime opa` gera Rego temporário e requer o executável `opa` disponível no PATH (ou em `PRD_OPA_BINARY`). OPA é um runtime candidato de conformance, não a autoridade semântica do modelo. + A [matriz de capacidades](framework/capabilities.json) é o contrato de escopo. **Não há conformidade OMG completa, motor BPMN, FEEL, prova modal, execução de step definitions, servidor MCP ou autenticação de revisores nesta versão.** ### Como os dados se relacionam @@ -116,6 +118,7 @@ Cenários Gherkin usam `SCN-...` e não duplicam um arquivo YAML de identidade. | `test --require-tests --require-bound-scenarios` | Executa casos de decisões e exige vínculos de cenários | | `decision evaluate ID --input '{...}'` | Avalia uma tabela suportada | | `decision analyze ID` | Analisa domínios finitos | +| `conformance decision ID --runtime reference|opa` | Compara um runtime executável com a semântica de referência | | `cite ID --into notes/design.md` | Preserva snapshot e cita conteúdo | | `citations check`, `citations refresh arquivo.md` | Verifica ou atualiza citações intactas | | `change new SLUG --title MOTIVO` | Captura baseline | From c0f09136f587e3f18354381a5ef958ef7876875f Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:29:09 -0300 Subject: [PATCH 10/12] docs(conformance): define runtime conformance semantics --- docs/TESTING.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/TESTING.md b/docs/TESTING.md index 02d7ff5..ad86894 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -12,6 +12,7 @@ | `npm test` | regressão automatizada do CLI, compilador e domínio | execução de uma aplicação externa | | `prd validate --strict` | schemas, referências e invariantes dos perfis | conformidade OMG completa | | `prd test` | resultados dos casos de decisões tipadas | execução dos passos Gherkin | +| `prd conformance decision ID --runtime opa` | equivalência dos casos entre a semântica de referência e o adapter OPA | equivalência para entradas fora dos casos declarados | | `prd citations check` | estado dos blocos e snapshots conhecidos | autenticidade do autor | Ao adicionar um tipo ou alterar um contrato, atualize em conjunto o schema, o @@ -36,3 +37,19 @@ O teste de distribuição deve usar um diretório fora do repositório de origem Isso detecta referências acidentais ao workspace de desenvolvimento que uma execução direta de `dist/` não detecta. + + +## Conformidade de runtimes de decisão + +`evaluateDecision()` continua sendo a semântica de referência do perfil +`dmn-table/v1`. A porta `DecisionRuntime` permite executar os mesmos casos em um +runtime candidato sem mover a autoridade semântica para esse runtime. + +```sh +prd conformance decision DEC-001 --runtime reference +prd conformance decision DEC-001 --runtime opa +``` + +O adapter OPA compila somente o subconjunto já suportado: tabela `UNIQUE`, +condições de igualdade, wildcard por condição omitida e saída escalar. Ele não +expande o perfil DMN. O executável `opa` é opcional e não é baixado pelo pacote. From 4150e3d57680abffb27ef310fdce92e25b27d054 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:29:11 -0300 Subject: [PATCH 11/12] docs(roadmap): place runtime conformance in 0.3 --- docs/ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 9f0da96..74b107c 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -6,11 +6,11 @@ CLI, compiler, schemas, perfis limitados, init com Copilot, modelo de exemplo, g ## 0.3 — Verificação do candidato -Adicionar testes de propriedade; análise incremental; inventário de citações por documento; suporte revisado a Outline. Saída: ampliar a cobertura semântica e medir regressões incrementais. O alpha já bloqueia approve/apply quando os casos de decisão do candidato falham. +Formalizar runtimes de decisão e conformance; adicionar testes de propriedade; análise incremental; inventário de citações por documento; suporte revisado a Outline. Saída: ampliar a cobertura semântica e medir regressões incrementais. O alpha já bloqueia approve/apply quando os casos de decisão do candidato falham. ## 0.4 — Execução e evidências -Adaptadores reais para step definitions e resultados de CI; manifest de evidência com commit/digest; comparação diferencial de decisões. Saída: distinguir formalmente testes do modelo e testes da aplicação nas APIs e relatórios. +Adaptadores reais para step definitions e resultados de CI; manifest de evidência com commit/digest; comparação diferencial com dados/fatos produzidos por aplicações. Saída: distinguir formalmente testes do modelo e testes da aplicação nas APIs e relatórios. ## 0.5 — Integrações From cf923a6365d85d779fc10e7b00cb8f0fbc9d90f5 Mon Sep 17 00:00:00 2001 From: Cleilson Date: Sat, 3 Oct 2026 00:29:14 -0300 Subject: [PATCH 12/12] docs(capabilities): declare decision conformance --- framework/capabilities.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/framework/capabilities.json b/framework/capabilities.json index a1cbc05..19e57fa 100644 --- a/framework/capabilities.json +++ b/framework/capabilities.json @@ -50,7 +50,9 @@ "omitted conditions as wildcard", "typed outputs", "finite domain exhaustiveness up to 4096 combinations", - "scenario-linked cases" + "scenario-linked cases", + "reference runtime conformance contract", + "optional OPA/Rego adapter for declared decision cases" ], "unsupported": [ "FEEL", @@ -112,6 +114,7 @@ "authenticated PR approval provider", "scenario step runner", "RDF/OWL exports", - "decision differential runner" + "external fact resolver conformance runner", + "historical decision differential runner" ] }