@cleocode/contracts 2026.5.121 → 2026.5.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/satisfies-atom.test.d.ts +24 -0
- package/dist/__tests__/satisfies-atom.test.d.ts.map +1 -0
- package/dist/__tests__/satisfies-atom.test.js +302 -0
- package/dist/__tests__/satisfies-atom.test.js.map +1 -0
- package/dist/__tests__/validator-types.test.d.ts +13 -0
- package/dist/__tests__/validator-types.test.d.ts.map +1 -0
- package/dist/__tests__/validator-types.test.js +293 -0
- package/dist/__tests__/validator-types.test.js.map +1 -0
- package/dist/config.d.ts +56 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/data-accessor.d.ts +117 -0
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/evidence-atom-schema.d.ts +91 -0
- package/dist/evidence-atom-schema.d.ts.map +1 -1
- package/dist/evidence-atom-schema.js +162 -1
- package/dist/evidence-atom-schema.js.map +1 -1
- package/dist/exit-codes.d.ts +41 -1
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +40 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/operations/tasks.d.ts +49 -0
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js.map +1 -1
- package/dist/task.d.ts +38 -0
- package/dist/task.d.ts.map +1 -1
- package/dist/validator/index.d.ts +375 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +162 -0
- package/dist/validator/index.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/satisfies-atom.test.ts +363 -0
- package/src/__tests__/validator-types.test.ts +359 -0
- package/src/config.ts +58 -0
- package/src/data-accessor.ts +120 -0
- package/src/evidence-atom-schema.ts +188 -1
- package/src/exit-codes.ts +42 -0
- package/src/index.ts +37 -0
- package/src/operations/tasks.ts +50 -0
- package/src/task.ts +39 -0
- package/src/validator/index.ts +385 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator role contracts — types consumed by the SDK tools (T10511) and the
|
|
3
|
+
* Max-N runtime (T10512) to express the verdict a Validator agent returns
|
|
4
|
+
* after reviewing a Worker submission against a task's acceptance criteria.
|
|
5
|
+
*
|
|
6
|
+
* The Validator is a NEW canonical role in the orchestration hierarchy,
|
|
7
|
+
* orthogonal to the existing Orchestrator → Lead → Worker triad. A Validator
|
|
8
|
+
* does NOT execute work and does NOT spawn other agents — it ONLY attests or
|
|
9
|
+
* rejects work claimed-done by another agent, per-AC.
|
|
10
|
+
*
|
|
11
|
+
* Identity triad:
|
|
12
|
+
* - `agentId` follows the pattern `validator-<discriminator>` (e.g.
|
|
13
|
+
* `validator-prime`, `validator-sec-001`).
|
|
14
|
+
* - `role` is `'validator'` (see {@link AgentRole}).
|
|
15
|
+
* - `sigilCard` is reused as-is from the existing peer/memory layer when
|
|
16
|
+
* the Validator is registered as a CANT-defined persona.
|
|
17
|
+
*
|
|
18
|
+
* Design constraints (ADR-055 / D028 boundary rules):
|
|
19
|
+
* - Lives in `packages/contracts/` — ZERO runtime dependencies (Zod only).
|
|
20
|
+
* - Consumed by `packages/cleo-sdk-tools/` (T10511) and `packages/core/` (T10512).
|
|
21
|
+
* - No cross-package relative imports.
|
|
22
|
+
*
|
|
23
|
+
* @module validator
|
|
24
|
+
* @task T10510
|
|
25
|
+
* @epic T10383 (E-VALIDATOR-ROLE)
|
|
26
|
+
* @saga T10377 (SG-IVTR-AC-BINDING)
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
/**
|
|
30
|
+
* Frozen list of canonical {@link AgentRole} values for runtime iteration
|
|
31
|
+
* (e.g. CLI flag validation, registry seeding).
|
|
32
|
+
*
|
|
33
|
+
* @task T10510
|
|
34
|
+
* @epic T10383
|
|
35
|
+
*/
|
|
36
|
+
export const AGENT_ROLES = ['orchestrator', 'lead', 'worker', 'validator'];
|
|
37
|
+
/**
|
|
38
|
+
* Type guard for {@link AgentRole}. Returns `true` when `value` is one of the
|
|
39
|
+
* four canonical role strings.
|
|
40
|
+
*
|
|
41
|
+
* @task T10510
|
|
42
|
+
*/
|
|
43
|
+
export function isAgentRole(value) {
|
|
44
|
+
return typeof value === 'string' && AGENT_ROLES.includes(value);
|
|
45
|
+
}
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Zod schemas — runtime validation
|
|
48
|
+
// ============================================================================
|
|
49
|
+
/**
|
|
50
|
+
* Regex validating the canonical Validator agentId pattern
|
|
51
|
+
* `validator-<discriminator>`. The discriminator is `[a-z0-9][a-z0-9-]*`
|
|
52
|
+
* (lowercase alphanumeric + hyphens, must start with alphanum).
|
|
53
|
+
*
|
|
54
|
+
* Examples that MATCH: `validator-prime`, `validator-sec-001`, `validator-a`.
|
|
55
|
+
* Examples that DO NOT match: `validator-`, `Validator-prime`, `validator--x`.
|
|
56
|
+
*
|
|
57
|
+
* @task T10510
|
|
58
|
+
*/
|
|
59
|
+
export const VALIDATOR_ID_REGEX = /^validator-[a-z0-9][a-z0-9-]*$/;
|
|
60
|
+
/**
|
|
61
|
+
* Zod schema for {@link ValidatorFinding}. Enforces the AC-id presence,
|
|
62
|
+
* status enum, non-empty reasoning, and ISO-8601 timestamp shape.
|
|
63
|
+
*
|
|
64
|
+
* @task T10510
|
|
65
|
+
*/
|
|
66
|
+
export const validatorFindingSchema = z.object({
|
|
67
|
+
acId: z.string().min(1, 'acId must be non-empty'),
|
|
68
|
+
status: z.enum(['pass', 'fail', 'inconclusive']),
|
|
69
|
+
reasoning: z.string().min(1, 'reasoning must be non-empty'),
|
|
70
|
+
evidenceRefs: z.array(z.string()).optional(),
|
|
71
|
+
checkedAt: z.string().min(1, 'checkedAt must be a non-empty ISO-8601 string'),
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Zod schema for {@link ValidatorAttestation}. Enforces:
|
|
75
|
+
* - `verdict === 'attest'`
|
|
76
|
+
* - `validatorId` matches {@link VALIDATOR_ID_REGEX}
|
|
77
|
+
* - `findings` non-empty AND every entry has `status === 'pass'`
|
|
78
|
+
* - `schemaVersion === '1'`
|
|
79
|
+
*
|
|
80
|
+
* @task T10510
|
|
81
|
+
*/
|
|
82
|
+
export const validatorAttestationSchema = z.object({
|
|
83
|
+
verdict: z.literal('attest'),
|
|
84
|
+
taskId: z.string().min(1),
|
|
85
|
+
validatorId: z
|
|
86
|
+
.string()
|
|
87
|
+
.regex(VALIDATOR_ID_REGEX, 'validatorId must match the pattern validator-<discriminator>'),
|
|
88
|
+
findings: z
|
|
89
|
+
.array(validatorFindingSchema)
|
|
90
|
+
.min(1, 'attestation must contain at least one finding')
|
|
91
|
+
.refine((findings) => findings.every((f) => f.status === 'pass'), 'attestation requires every finding to have status="pass"'),
|
|
92
|
+
summary: z.string().optional(),
|
|
93
|
+
attestedAt: z.string().min(1),
|
|
94
|
+
schemaVersion: z.literal('1'),
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Zod schema for {@link ValidatorRejection}. Enforces:
|
|
98
|
+
* - `verdict === 'reject'`
|
|
99
|
+
* - `validatorId` matches {@link VALIDATOR_ID_REGEX}
|
|
100
|
+
* - `findings` non-empty AND at least one entry has `status !== 'pass'`
|
|
101
|
+
* - `summary` non-empty (rejection rationale is mandatory)
|
|
102
|
+
* - `schemaVersion === '1'`
|
|
103
|
+
*
|
|
104
|
+
* @task T10510
|
|
105
|
+
*/
|
|
106
|
+
export const validatorRejectionSchema = z.object({
|
|
107
|
+
verdict: z.literal('reject'),
|
|
108
|
+
taskId: z.string().min(1),
|
|
109
|
+
validatorId: z
|
|
110
|
+
.string()
|
|
111
|
+
.regex(VALIDATOR_ID_REGEX, 'validatorId must match the pattern validator-<discriminator>'),
|
|
112
|
+
findings: z
|
|
113
|
+
.array(validatorFindingSchema)
|
|
114
|
+
.min(1, 'rejection must contain at least one finding')
|
|
115
|
+
.refine((findings) => findings.some((f) => f.status !== 'pass'), 'rejection requires at least one finding with status "fail" or "inconclusive"'),
|
|
116
|
+
summary: z.string().min(1, 'rejection summary must be non-empty'),
|
|
117
|
+
remediationHints: z.array(z.string()).optional(),
|
|
118
|
+
rejectedAt: z.string().min(1),
|
|
119
|
+
schemaVersion: z.literal('1'),
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* Zod schema for the {@link ValidatorVerdict} discriminated union.
|
|
123
|
+
* Routes parsing through the `verdict` discriminant.
|
|
124
|
+
*
|
|
125
|
+
* @task T10510
|
|
126
|
+
*/
|
|
127
|
+
export const validatorVerdictSchema = z.discriminatedUnion('verdict', [
|
|
128
|
+
validatorAttestationSchema,
|
|
129
|
+
validatorRejectionSchema,
|
|
130
|
+
]);
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Type guards
|
|
133
|
+
// ============================================================================
|
|
134
|
+
/**
|
|
135
|
+
* Type guard validating that `value` conforms to {@link ValidatorAttestation}.
|
|
136
|
+
* Internally uses {@link validatorAttestationSchema}.
|
|
137
|
+
*
|
|
138
|
+
* @task T10510
|
|
139
|
+
*/
|
|
140
|
+
export function isValidatorAttestation(value) {
|
|
141
|
+
return validatorAttestationSchema.safeParse(value).success;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Type guard validating that `value` conforms to {@link ValidatorRejection}.
|
|
145
|
+
* Internally uses {@link validatorRejectionSchema}.
|
|
146
|
+
*
|
|
147
|
+
* @task T10510
|
|
148
|
+
*/
|
|
149
|
+
export function isValidatorRejection(value) {
|
|
150
|
+
return validatorRejectionSchema.safeParse(value).success;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Type guard validating that `value` conforms to {@link ValidatorVerdict}
|
|
154
|
+
* (either an attestation or a rejection). Internally uses
|
|
155
|
+
* {@link validatorVerdictSchema}.
|
|
156
|
+
*
|
|
157
|
+
* @task T10510
|
|
158
|
+
*/
|
|
159
|
+
export function isValidatorVerdict(value) {
|
|
160
|
+
return validatorVerdictSchema.safeParse(value).success;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/validator/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/contracts",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.122",
|
|
4
4
|
"description": "Domain types, interfaces, and contracts for the CLEO ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"dependencies": {
|
|
139
139
|
"zod": "^4.3.6",
|
|
140
140
|
"zod-to-json-schema": "^3.25.2",
|
|
141
|
-
"@cleocode/lafs": "2026.5.
|
|
141
|
+
"@cleocode/lafs": "2026.5.122"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "tsc -b --force && node scripts/copy-assets.mjs && node scripts/emit-schemas.mjs",
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ADR-079-r2 `satisfies:<task-id>#<ac-id>` evidence atom
|
|
3
|
+
* grammar (T10506).
|
|
4
|
+
*
|
|
5
|
+
* Coverage:
|
|
6
|
+
* - Schema-level: per-field regex validation (task-id, ac-uuid, ac-alias,
|
|
7
|
+
* version-pin) and discriminated-union acceptance.
|
|
8
|
+
* - Parser-level: full {@link parseEvidenceString} round-trips for both
|
|
9
|
+
* UUID and alias forms, mixed evidence strings, and the optional
|
|
10
|
+
* `@<version-pin>` suffix.
|
|
11
|
+
* - Malformed inputs: missing colon, missing hash, missing T-prefix,
|
|
12
|
+
* mixed-case UUID, non-UUID non-alias ac-id, malformed version-pin.
|
|
13
|
+
*
|
|
14
|
+
* This file covers PARSING ONLY per T10506 acceptance criteria. The 5-check
|
|
15
|
+
* validator semantics (target exists, target not terminal, AC exists, same-
|
|
16
|
+
* saga scope rule, alias drift detection) ship in T10507 and are tested
|
|
17
|
+
* separately in `packages/core/src/tasks/__tests__/satisfies-validator.test.ts`.
|
|
18
|
+
*
|
|
19
|
+
* @task T10506
|
|
20
|
+
* @saga T10377
|
|
21
|
+
* @adr ADR-079-r2
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, expect, it } from 'vitest';
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
AC_ALIAS_REGEX,
|
|
28
|
+
AC_UUID_REGEX,
|
|
29
|
+
EvidenceAtomSchema,
|
|
30
|
+
EvidenceParseError,
|
|
31
|
+
parseEvidenceString,
|
|
32
|
+
SATISFIES_TASK_ID_REGEX,
|
|
33
|
+
SATISFIES_VERSION_PIN_REGEX,
|
|
34
|
+
satisfiesAtomSchema,
|
|
35
|
+
} from '../index.js';
|
|
36
|
+
|
|
37
|
+
// ─── Per-field regex parity (ADR-079-r2 §2.1 ABNF) ────────────────────────
|
|
38
|
+
|
|
39
|
+
describe('satisfies atom regexes (ADR-079-r2 §2.1)', () => {
|
|
40
|
+
describe('SATISFIES_TASK_ID_REGEX', () => {
|
|
41
|
+
it('accepts T<1-7 digits>', () => {
|
|
42
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T1')).toBe(true);
|
|
43
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T1234')).toBe(true);
|
|
44
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T1234567')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('rejects T<8+ digits>', () => {
|
|
47
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T12345678')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it('rejects missing T prefix', () => {
|
|
50
|
+
expect(SATISFIES_TASK_ID_REGEX.test('1234')).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('rejects lowercase t', () => {
|
|
53
|
+
expect(SATISFIES_TASK_ID_REGEX.test('t1234')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it('rejects non-digit suffix', () => {
|
|
56
|
+
expect(SATISFIES_TASK_ID_REGEX.test('T12a')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('AC_UUID_REGEX', () => {
|
|
61
|
+
it('accepts strict lowercase UUIDv4', () => {
|
|
62
|
+
expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-4890-abcd-ef1234567890')).toBe(true);
|
|
63
|
+
expect(AC_UUID_REGEX.test('00000000-0000-4000-8000-000000000000')).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
it('rejects mixed-case', () => {
|
|
66
|
+
// Council-mandated strictness (ADR-079-r2 §2.2): "Validators MUST
|
|
67
|
+
// reject mixed-case to prevent silent dedupe failures."
|
|
68
|
+
expect(AC_UUID_REGEX.test('A1B2C3D4-5E6F-4890-ABCD-EF1234567890')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
it('rejects wrong version nibble (not 4)', () => {
|
|
71
|
+
expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-5890-abcd-ef1234567890')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('rejects wrong variant nibble (not 8/9/a/b)', () => {
|
|
74
|
+
expect(AC_UUID_REGEX.test('a1b2c3d4-5e6f-4890-cbcd-ef1234567890')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
it('rejects non-UUID strings', () => {
|
|
77
|
+
expect(AC_UUID_REGEX.test('not-a-uuid')).toBe(false);
|
|
78
|
+
expect(AC_UUID_REGEX.test('a1b2c3d45e6f4890abcdef1234567890')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('AC_ALIAS_REGEX', () => {
|
|
83
|
+
it('accepts AC<1-4 digits>', () => {
|
|
84
|
+
expect(AC_ALIAS_REGEX.test('AC1')).toBe(true);
|
|
85
|
+
expect(AC_ALIAS_REGEX.test('AC9999')).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
it('rejects AC<5+ digits>', () => {
|
|
88
|
+
expect(AC_ALIAS_REGEX.test('AC10000')).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
it('rejects lowercase ac', () => {
|
|
91
|
+
expect(AC_ALIAS_REGEX.test('ac1')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it('rejects bare integer', () => {
|
|
94
|
+
expect(AC_ALIAS_REGEX.test('1')).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('SATISFIES_VERSION_PIN_REGEX', () => {
|
|
99
|
+
it('accepts exactly 14 digits', () => {
|
|
100
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('20260524223045')).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
it('rejects fewer than 14 digits', () => {
|
|
103
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('2026052422304')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
it('rejects more than 14 digits', () => {
|
|
106
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('202605242230451')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('rejects non-digit characters', () => {
|
|
109
|
+
expect(SATISFIES_VERSION_PIN_REGEX.test('2026-05-24T22:30')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ─── Zod schema (post-parse client-side validation) ────────────────────────
|
|
115
|
+
|
|
116
|
+
describe('satisfiesAtomSchema (ADR-079-r2 §5.2)', () => {
|
|
117
|
+
it('accepts UUID form without version-pin', () => {
|
|
118
|
+
const a = satisfiesAtomSchema.parse({
|
|
119
|
+
kind: 'satisfies',
|
|
120
|
+
targetTaskId: 'T10495',
|
|
121
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
122
|
+
});
|
|
123
|
+
expect(a.targetTaskId).toBe('T10495');
|
|
124
|
+
expect(a.targetAcId).toBe('8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e');
|
|
125
|
+
expect(a.targetAcAlias).toBeUndefined();
|
|
126
|
+
expect(a.versionPin).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('accepts alias form without version-pin', () => {
|
|
130
|
+
const a = satisfiesAtomSchema.parse({
|
|
131
|
+
kind: 'satisfies',
|
|
132
|
+
targetTaskId: 'T10495',
|
|
133
|
+
targetAcAlias: 'AC2',
|
|
134
|
+
});
|
|
135
|
+
expect(a.targetAcAlias).toBe('AC2');
|
|
136
|
+
expect(a.targetAcId).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('accepts alias form with version-pin', () => {
|
|
140
|
+
const a = satisfiesAtomSchema.parse({
|
|
141
|
+
kind: 'satisfies',
|
|
142
|
+
targetTaskId: 'T10495',
|
|
143
|
+
targetAcAlias: 'AC2',
|
|
144
|
+
versionPin: '20260524223045',
|
|
145
|
+
});
|
|
146
|
+
expect(a.versionPin).toBe('20260524223045');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('accepts UUID form with version-pin (highest-trust)', () => {
|
|
150
|
+
const a = satisfiesAtomSchema.parse({
|
|
151
|
+
kind: 'satisfies',
|
|
152
|
+
targetTaskId: 'T10495',
|
|
153
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
154
|
+
versionPin: '20260524223045',
|
|
155
|
+
});
|
|
156
|
+
expect(a.targetAcId).toBeDefined();
|
|
157
|
+
expect(a.versionPin).toBe('20260524223045');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('rejects malformed targetTaskId via schema', () => {
|
|
161
|
+
const r = satisfiesAtomSchema.safeParse({
|
|
162
|
+
kind: 'satisfies',
|
|
163
|
+
targetTaskId: 'task-1234',
|
|
164
|
+
targetAcAlias: 'AC2',
|
|
165
|
+
});
|
|
166
|
+
expect(r.success).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('rejects mixed-case UUID via schema', () => {
|
|
170
|
+
const r = satisfiesAtomSchema.safeParse({
|
|
171
|
+
kind: 'satisfies',
|
|
172
|
+
targetTaskId: 'T10495',
|
|
173
|
+
targetAcId: '8F4A2C1E-B09D-4F6A-9C3E-7A1D4F8C0B2E',
|
|
174
|
+
});
|
|
175
|
+
expect(r.success).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('rejects malformed version-pin via schema', () => {
|
|
179
|
+
const r = satisfiesAtomSchema.safeParse({
|
|
180
|
+
kind: 'satisfies',
|
|
181
|
+
targetTaskId: 'T10495',
|
|
182
|
+
targetAcAlias: 'AC2',
|
|
183
|
+
versionPin: '2026-05-24T22:30',
|
|
184
|
+
});
|
|
185
|
+
expect(r.success).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('round-trips through the EvidenceAtomSchema discriminated union', () => {
|
|
189
|
+
const a = EvidenceAtomSchema.parse({
|
|
190
|
+
kind: 'satisfies',
|
|
191
|
+
targetTaskId: 'T10495',
|
|
192
|
+
targetAcAlias: 'AC2',
|
|
193
|
+
});
|
|
194
|
+
expect(a.kind).toBe('satisfies');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ─── parseEvidenceString (CLI parser) ──────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
describe('parseEvidenceString — satisfies atom (T10506)', () => {
|
|
201
|
+
// ─── Valid forms ─────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
it('parses UUID form', () => {
|
|
204
|
+
const atoms = parseEvidenceString('satisfies:T10495#8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e');
|
|
205
|
+
expect(atoms).toHaveLength(1);
|
|
206
|
+
expect(atoms[0]).toEqual({
|
|
207
|
+
kind: 'satisfies',
|
|
208
|
+
targetTaskId: 'T10495',
|
|
209
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('parses alias form', () => {
|
|
214
|
+
const atoms = parseEvidenceString('satisfies:T10495#AC2');
|
|
215
|
+
expect(atoms).toHaveLength(1);
|
|
216
|
+
expect(atoms[0]).toEqual({
|
|
217
|
+
kind: 'satisfies',
|
|
218
|
+
targetTaskId: 'T10495',
|
|
219
|
+
targetAcAlias: 'AC2',
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('parses alias form with version-pin', () => {
|
|
224
|
+
const atoms = parseEvidenceString('satisfies:T10495#AC2@20260524223045');
|
|
225
|
+
expect(atoms).toHaveLength(1);
|
|
226
|
+
expect(atoms[0]).toEqual({
|
|
227
|
+
kind: 'satisfies',
|
|
228
|
+
targetTaskId: 'T10495',
|
|
229
|
+
targetAcAlias: 'AC2',
|
|
230
|
+
versionPin: '20260524223045',
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('parses UUID form with version-pin (canonical highest-trust)', () => {
|
|
235
|
+
const atoms = parseEvidenceString(
|
|
236
|
+
'satisfies:T10495#8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e@20260524223045',
|
|
237
|
+
);
|
|
238
|
+
expect(atoms).toHaveLength(1);
|
|
239
|
+
expect(atoms[0]).toEqual({
|
|
240
|
+
kind: 'satisfies',
|
|
241
|
+
targetTaskId: 'T10495',
|
|
242
|
+
targetAcId: '8f4a2c1e-b09d-4f6a-9c3e-7a1d4f8c0b2e',
|
|
243
|
+
versionPin: '20260524223045',
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('parses multiple satisfies atoms in one evidence string', () => {
|
|
248
|
+
const atoms = parseEvidenceString('satisfies:T10495#AC2;satisfies:T10493#AC1');
|
|
249
|
+
expect(atoms).toHaveLength(2);
|
|
250
|
+
expect(atoms[0]).toMatchObject({
|
|
251
|
+
kind: 'satisfies',
|
|
252
|
+
targetTaskId: 'T10495',
|
|
253
|
+
targetAcAlias: 'AC2',
|
|
254
|
+
});
|
|
255
|
+
expect(atoms[1]).toMatchObject({
|
|
256
|
+
kind: 'satisfies',
|
|
257
|
+
targetTaskId: 'T10493',
|
|
258
|
+
targetAcAlias: 'AC1',
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('parses mixed evidence: commit + files + satisfies', () => {
|
|
263
|
+
const atoms = parseEvidenceString(
|
|
264
|
+
'commit:abc1234;files:src/foo.ts,src/bar.ts;satisfies:T10495#AC2',
|
|
265
|
+
);
|
|
266
|
+
expect(atoms).toHaveLength(3);
|
|
267
|
+
expect(atoms[0]).toEqual({ kind: 'commit', sha: 'abc1234' });
|
|
268
|
+
expect(atoms[1]).toEqual({ kind: 'files', paths: ['src/foo.ts', 'src/bar.ts'] });
|
|
269
|
+
expect(atoms[2]).toEqual({
|
|
270
|
+
kind: 'satisfies',
|
|
271
|
+
targetTaskId: 'T10495',
|
|
272
|
+
targetAcAlias: 'AC2',
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('parses mixed evidence: satisfies + pr (single PR landing multiple ACs)', () => {
|
|
277
|
+
const atoms = parseEvidenceString('satisfies:T10506#AC1;satisfies:T10506#AC2;pr:773');
|
|
278
|
+
expect(atoms).toHaveLength(3);
|
|
279
|
+
expect(atoms[2]).toEqual({ kind: 'pr', prNumber: 773 });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─── Malformed: missing colon ────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
it('rejects atom missing the kind-payload colon', () => {
|
|
285
|
+
// "satisfiesT10495#AC2" — no `:` separator at all
|
|
286
|
+
expect(() => parseEvidenceString('satisfiesT10495#AC2')).toThrow(EvidenceParseError);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ─── Malformed: missing hash ─────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
it('rejects atom missing the # separator between task-id and ac-id', () => {
|
|
292
|
+
expect(() => parseEvidenceString('satisfies:T10495-AC2')).toThrow(EvidenceParseError);
|
|
293
|
+
expect(() => parseEvidenceString('satisfies:T10495AC2')).toThrow(EvidenceParseError);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('rejects atom with # at start of payload (empty task-id)', () => {
|
|
297
|
+
expect(() => parseEvidenceString('satisfies:#AC2')).toThrow(EvidenceParseError);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('rejects atom with # at end of payload (empty ac-id)', () => {
|
|
301
|
+
expect(() => parseEvidenceString('satisfies:T10495#')).toThrow(EvidenceParseError);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ─── Malformed: missing T-prefix on task-id ──────────────────────────────
|
|
305
|
+
|
|
306
|
+
it('rejects atom with non-T-prefixed task-id', () => {
|
|
307
|
+
expect(() => parseEvidenceString('satisfies:10495#AC2')).toThrow(EvidenceParseError);
|
|
308
|
+
expect(() => parseEvidenceString('satisfies:task-10495#AC2')).toThrow(EvidenceParseError);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('rejects atom with lowercase t prefix', () => {
|
|
312
|
+
expect(() => parseEvidenceString('satisfies:t10495#AC2')).toThrow(EvidenceParseError);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('rejects atom with task-id exceeding 7 digits', () => {
|
|
316
|
+
expect(() => parseEvidenceString('satisfies:T12345678#AC2')).toThrow(EvidenceParseError);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ─── Malformed: ac-id ────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
it('rejects atom with mixed-case UUID', () => {
|
|
322
|
+
// Council §2.2 strictness — silent dedupe prevention
|
|
323
|
+
expect(() =>
|
|
324
|
+
parseEvidenceString('satisfies:T10495#8F4A2C1E-B09D-4F6A-9C3E-7A1D4F8C0B2E'),
|
|
325
|
+
).toThrow(EvidenceParseError);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('rejects atom with ac-id that is neither UUID nor AC<digits> alias', () => {
|
|
329
|
+
expect(() => parseEvidenceString('satisfies:T10495#hello')).toThrow(EvidenceParseError);
|
|
330
|
+
expect(() => parseEvidenceString('satisfies:T10495#2')).toThrow(EvidenceParseError);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('rejects atom with alias exceeding 4 digits', () => {
|
|
334
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC10000')).toThrow(EvidenceParseError);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ─── Malformed: version-pin ──────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
it('rejects atom with malformed version-pin (not 14 digits)', () => {
|
|
340
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC2@2026-05-24')).toThrow(
|
|
341
|
+
EvidenceParseError,
|
|
342
|
+
);
|
|
343
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC2@2026052422304')).toThrow(
|
|
344
|
+
EvidenceParseError,
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('rejects atom with @ but empty version-pin payload', () => {
|
|
349
|
+
expect(() => parseEvidenceString('satisfies:T10495#AC2@')).toThrow(EvidenceParseError);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// ─── Error message smoke: ensures kind name appears in default-case ──────
|
|
353
|
+
|
|
354
|
+
it('includes "satisfies" in the unknown-kind error suggestion list', () => {
|
|
355
|
+
try {
|
|
356
|
+
parseEvidenceString('unknownkind:foo');
|
|
357
|
+
throw new Error('expected EvidenceParseError');
|
|
358
|
+
} catch (err) {
|
|
359
|
+
expect(err).toBeInstanceOf(EvidenceParseError);
|
|
360
|
+
expect((err as EvidenceParseError).fix).toContain('satisfies');
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|