Skip to content

Commit f67eec7

Browse files
os-zhuangclaude
andcommitted
feat(dogfood): ADR-0060 P2 — validation-rule conformance ledger (pins ADR-0020)
The third instance of the reusable conformance-ledger pattern, built on the same checkLedger helper (P1) — proving a new surface is a filled-in table, not a re-derived discipline. One row per `validations` union rule type (state_machine / script / cross_field / format / json_schema / conditional), all ENFORCED on the engine write path via rule-validator.ts (ADR-0020 wired the union in), each carrying the rule-validator runtime proof. The ratchet re-discovers every rule type from validation.zod.ts's discriminator and fails the build if one is unclassified — so a new validation rule type added without enforcement (the pre-ADR-0020 state_machine disease) can't ship silently. Ratchet verified to bite. dogfood-private, no changeset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ee86099 commit f67eec7

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0060 P2 — Validation-Rule Conformance ledger (the third instance of the
4+
// reusable pattern). One row per `validations` union rule type, each pinned to
5+
// its runtime enforcement site and a proof. ADR-0020 wired this whole union into
6+
// the write path (engine insert/update → rule-validator.ts), turning every rule
7+
// type from declaration into enforcement; this ledger makes that a CHECKED fact
8+
// and — via the ratchet — makes a NEW rule type with no enforcement a build break
9+
// (the exact pre-ADR-0020 `state_machine` disease).
10+
11+
import type { ConformanceRow } from '@objectstack/verify';
12+
13+
const RULE_VALIDATOR = 'packages/objectql/src/validation/rule-validator.ts';
14+
const PROOF = 'packages/objectql/src/validation/rule-validator.test.ts';
15+
16+
export const VALIDATION_SURFACE: ConformanceRow[] = [
17+
{
18+
id: 'rule-state-machine',
19+
summary: 'state_machine — legal status-transition guard (ADR-0020 D3)',
20+
surface: 'validation.zod.ts:state_machine',
21+
state: 'enforced',
22+
enforcement: `${RULE_VALIDATOR} checkStateMachine — engine insert/update path rejects an illegal from→to transition`,
23+
covers: ['state_machine'],
24+
proof: PROOF,
25+
},
26+
{
27+
id: 'rule-script',
28+
summary: 'script — arbitrary CEL failure predicate',
29+
surface: 'validation.zod.ts:script',
30+
state: 'enforced',
31+
enforcement: `${RULE_VALIDATOR} checkPredicate — CEL predicate; TRUE = violation`,
32+
covers: ['script'],
33+
proof: PROOF,
34+
},
35+
{
36+
id: 'rule-cross-field',
37+
summary: 'cross_field — multi-field CEL invariant',
38+
surface: 'validation.zod.ts:cross_field',
39+
state: 'enforced',
40+
enforcement: `${RULE_VALIDATOR} checkPredicate — evaluated against the merged record (prior ∪ change)`,
41+
covers: ['cross_field'],
42+
proof: PROOF,
43+
},
44+
{
45+
id: 'rule-format',
46+
summary: 'format — value-shape rule (regex/builtin)',
47+
surface: 'validation.zod.ts:format',
48+
state: 'enforced',
49+
enforcement: `${RULE_VALIDATOR} checkFormat`,
50+
covers: ['format'],
51+
proof: PROOF,
52+
},
53+
{
54+
id: 'rule-json-schema',
55+
summary: 'json_schema — structural validation of a JSON field',
56+
surface: 'validation.zod.ts:json_schema',
57+
state: 'enforced',
58+
enforcement: `${RULE_VALIDATOR} checkJsonSchema`,
59+
covers: ['json_schema'],
60+
proof: PROOF,
61+
},
62+
{
63+
id: 'rule-conditional',
64+
summary: 'conditional — when/then predicate rule',
65+
surface: 'validation.zod.ts:conditional',
66+
state: 'enforced',
67+
enforcement: `${RULE_VALIDATOR} checkConditional`,
68+
covers: ['conditional'],
69+
proof: PROOF,
70+
},
71+
];
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// ADR-0060 P2 — the validation-rule conformance ledger is a CHECKED artifact,
4+
// built on the SAME reusable checkLedger helper as the authz and expression
5+
// ledgers (proving the pattern composes: a new surface is a filled-in table, not
6+
// a re-derived discipline). The ratchet re-discovers every `validations` union
7+
// rule type from spec source and fails the build if any is unclassified — so a
8+
// new validation rule type added without runtime enforcement (the pre-ADR-0020
9+
// `state_machine` failure) can't ship silently.
10+
11+
import { describe, expect, it } from 'vitest';
12+
import { readFileSync } from 'node:fs';
13+
import { fileURLToPath } from 'node:url';
14+
import { dirname, join } from 'node:path';
15+
import { checkLedger } from '@objectstack/verify';
16+
import { VALIDATION_SURFACE } from './validation-conformance.ledger.js';
17+
18+
const HERE = dirname(fileURLToPath(import.meta.url));
19+
const REPO_ROOT = join(HERE, '../../..');
20+
const VALIDATION_ZOD = join(REPO_ROOT, 'packages/spec/src/data/validation.zod.ts');
21+
22+
/** Re-discover every `validations` union rule type from its discriminator. */
23+
function discoverRuleTypes(): Set<string> {
24+
const src = readFileSync(VALIDATION_ZOD, 'utf8');
25+
const found = new Set<string>();
26+
for (const m of src.matchAll(/type:\s*z\.literal\('([a-z_]+)'\)/g)) found.add(m[1]);
27+
return found;
28+
}
29+
30+
describe('ADR-0060 P2 — validation-rule conformance ledger', () => {
31+
it('is a sound conformance ledger + ratchet, every rule type enforced + proven', () => {
32+
const problems = checkLedger(VALIDATION_SURFACE, {
33+
proofRoot: REPO_ROOT,
34+
discover: discoverRuleTypes,
35+
proofRequiredForEnforced: true, // a write-path guard must carry a runtime proof
36+
});
37+
expect(problems, problems.join('\n')).toEqual([]);
38+
});
39+
40+
it('sanity: discovery finds the known rule types', () => {
41+
const found = discoverRuleTypes();
42+
for (const t of ['state_machine', 'script', 'cross_field', 'format', 'json_schema', 'conditional']) {
43+
expect(found.has(t), `rule type '${t}' not discovered`).toBe(true);
44+
}
45+
});
46+
});

0 commit comments

Comments
 (0)