thoth-agents 0.5.1 → 0.7.0
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/README.md +88 -11
- package/dist/agents/prompt-dialects.d.ts +1 -0
- package/dist/{chunk-MRFCA3U2.js → chunk-3MHJGWS4.js} +2 -1
- package/dist/{chunk-V7HSGRMB.js → chunk-P3GDM5RH.js} +70 -174
- package/dist/{chunk-OW6I7ZUU.js → chunk-QOIZYJ5C.js} +322 -151
- package/dist/{chunk-VUNYH263.js → chunk-W5TSNGEE.js} +263 -44
- package/dist/cli/index.js +4 -4
- package/dist/cli/pi-install.d.ts +35 -67
- package/dist/cli/pi-tool-config.d.ts +19 -0
- package/dist/cli/tui/index.js +3 -3
- package/dist/config/constants.d.ts +30 -3
- package/dist/config/schema.d.ts +4 -0
- package/dist/harness/adapters/claude-code.d.ts +1 -1
- package/dist/harness/core/agent-pack.d.ts +6 -2
- package/dist/harness/core/result-contract.d.ts +366 -0
- package/dist/harness/core/sdd.d.ts +1 -1
- package/dist/harness/writers/pi-agent.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/pi.d.ts +6 -35
- package/dist/pi.js +37 -433
- package/package.json +5 -3
- package/pi/.thoth-agents-assets.json +5 -5
- package/pi/agents/thoth-designer.md +15 -20
- package/pi/agents/thoth-explorer.md +15 -19
- package/pi/agents/thoth-librarian.md +15 -19
- package/pi/agents/thoth-oracle.md +30 -22
- package/pi/agents/thoth-worker.md +13 -18
- package/plugin/.claude-plugin/.thoth-agents-plugin-assets.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/agents/designer.md +9 -14
- package/plugin/agents/explorer.md +10 -14
- package/plugin/agents/librarian.md +9 -13
- package/plugin/agents/oracle.md +24 -18
- package/plugin/agents/orchestrator.md +38 -61
- package/plugin/agents/worker.md +9 -14
- package/plugin/skills/plan-reviewer/SKILL.md +16 -8
- package/plugin/skills/thoth-archive/SKILL.md +2 -1
- package/plugin/skills/thoth-constitution/SKILL.md +1 -1
- package/plugin/skills/thoth-constitution/templates/constitution.md +1 -1
- package/plugin/skills/thoth-init/SKILL.md +1 -1
- package/plugin/skills/thoth-sdd/SKILL.md +67 -2
- package/plugin/skills/thoth-sdd/references/phases/verify.md +11 -1
- package/plugin/skills/thoth-sdd/scripts/result-contract.json +210 -0
- package/plugin/skills/thoth-sdd/scripts/validate-result.d.mts +47 -0
- package/plugin/skills/thoth-sdd/scripts/validate-result.mjs +390 -0
- package/plugin/skills/thoth-sdd/scripts/validate.mjs +213 -8
- package/plugin/skills/thoth-sdd/templates/change.md +8 -0
- package/skills/plan-reviewer/SKILL.md +16 -8
- package/skills/thoth-archive/SKILL.md +2 -1
- package/skills/thoth-constitution/SKILL.md +1 -1
- package/skills/thoth-constitution/templates/constitution.md +1 -1
- package/skills/thoth-init/SKILL.md +1 -1
- package/skills/thoth-sdd/SKILL.md +67 -2
- package/skills/thoth-sdd/references/phases/verify.md +11 -1
- package/skills/thoth-sdd/scripts/result-contract.json +210 -0
- package/skills/thoth-sdd/scripts/validate-result.d.mts +47 -0
- package/skills/thoth-sdd/scripts/validate-result.mjs +390 -0
- package/skills/thoth-sdd/scripts/validate.mjs +213 -8
- package/skills/thoth-sdd/templates/change.md +8 -0
- package/thoth-agents.schema.json +14 -0
- package/dist/pi/tools-panel.d.ts +0 -51
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"fence": "thoth-result",
|
|
4
|
+
"common": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"schemaVersion": {
|
|
8
|
+
"type": "number",
|
|
9
|
+
"const": 1
|
|
10
|
+
},
|
|
11
|
+
"role": {
|
|
12
|
+
"type": "string",
|
|
13
|
+
"enum": ["oracle"]
|
|
14
|
+
},
|
|
15
|
+
"status": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["complete", "partial", "blocked", "failed"]
|
|
18
|
+
},
|
|
19
|
+
"conclusion": {
|
|
20
|
+
"type": "string"
|
|
21
|
+
},
|
|
22
|
+
"evidence": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"items": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"id": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"minLength": 1
|
|
30
|
+
},
|
|
31
|
+
"kind": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
},
|
|
34
|
+
"locator": {
|
|
35
|
+
"type": "string"
|
|
36
|
+
},
|
|
37
|
+
"claim": {
|
|
38
|
+
"type": "string"
|
|
39
|
+
},
|
|
40
|
+
"basis": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["observed", "inferred"]
|
|
43
|
+
},
|
|
44
|
+
"uncertainty": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"required": [
|
|
49
|
+
"id",
|
|
50
|
+
"kind",
|
|
51
|
+
"locator",
|
|
52
|
+
"claim",
|
|
53
|
+
"basis",
|
|
54
|
+
"uncertainty"
|
|
55
|
+
],
|
|
56
|
+
"additionalProperties": false
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"verification": {
|
|
60
|
+
"type": "array",
|
|
61
|
+
"items": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"properties": {
|
|
64
|
+
"check": {
|
|
65
|
+
"type": "string"
|
|
66
|
+
},
|
|
67
|
+
"method": {
|
|
68
|
+
"type": "string"
|
|
69
|
+
},
|
|
70
|
+
"outcome": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": ["pass", "fail", "not-run"]
|
|
73
|
+
},
|
|
74
|
+
"evidenceIds": {
|
|
75
|
+
"type": "array",
|
|
76
|
+
"items": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"reason": {
|
|
81
|
+
"type": "string"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"required": ["check", "method", "outcome", "evidenceIds", "reason"],
|
|
85
|
+
"additionalProperties": false
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"risks": {
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"description": {
|
|
94
|
+
"type": "string"
|
|
95
|
+
},
|
|
96
|
+
"severity": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"enum": ["low", "medium", "high", "critical"]
|
|
99
|
+
},
|
|
100
|
+
"blocking": {
|
|
101
|
+
"type": "boolean"
|
|
102
|
+
},
|
|
103
|
+
"evidenceIds": {
|
|
104
|
+
"type": "array",
|
|
105
|
+
"items": {
|
|
106
|
+
"type": "string"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"required": ["description", "severity", "blocking", "evidenceIds"],
|
|
111
|
+
"additionalProperties": false
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"openQuestions": {
|
|
115
|
+
"type": "array",
|
|
116
|
+
"items": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"question": {
|
|
120
|
+
"type": "string"
|
|
121
|
+
},
|
|
122
|
+
"owner": {
|
|
123
|
+
"type": "string"
|
|
124
|
+
},
|
|
125
|
+
"options": {
|
|
126
|
+
"type": "array",
|
|
127
|
+
"items": {
|
|
128
|
+
"type": "string"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"facts": {
|
|
132
|
+
"type": "array",
|
|
133
|
+
"items": {
|
|
134
|
+
"type": "string"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
"required": ["question", "owner", "options", "facts"],
|
|
139
|
+
"additionalProperties": false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"required": [
|
|
144
|
+
"schemaVersion",
|
|
145
|
+
"role",
|
|
146
|
+
"status",
|
|
147
|
+
"conclusion",
|
|
148
|
+
"evidence",
|
|
149
|
+
"verification",
|
|
150
|
+
"risks",
|
|
151
|
+
"openQuestions"
|
|
152
|
+
],
|
|
153
|
+
"additionalProperties": false
|
|
154
|
+
},
|
|
155
|
+
"roles": {
|
|
156
|
+
"oracle": {
|
|
157
|
+
"type": "object",
|
|
158
|
+
"properties": {
|
|
159
|
+
"nextAction": {
|
|
160
|
+
"type": ["string", "null"]
|
|
161
|
+
},
|
|
162
|
+
"review": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"properties": {
|
|
165
|
+
"kind": {
|
|
166
|
+
"type": "string",
|
|
167
|
+
"enum": ["plan-review", "final-verification"]
|
|
168
|
+
},
|
|
169
|
+
"verdict": {
|
|
170
|
+
"type": "string"
|
|
171
|
+
},
|
|
172
|
+
"reviewedRecord": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"minLength": 1
|
|
175
|
+
},
|
|
176
|
+
"reviewedRecordSha256": {
|
|
177
|
+
"type": "string",
|
|
178
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
"required": ["kind", "verdict"],
|
|
182
|
+
"additionalProperties": false
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
"required": ["nextAction", "review"],
|
|
186
|
+
"additionalProperties": false
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"reviewKinds": {
|
|
190
|
+
"plan-review": {
|
|
191
|
+
"verdicts": ["OKAY", "REJECT"]
|
|
192
|
+
},
|
|
193
|
+
"final-verification": {
|
|
194
|
+
"verdicts": ["PASS", "FAIL", "BLOCKED"]
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
"rules": {
|
|
198
|
+
"positiveVerdicts": ["OKAY", "PASS"],
|
|
199
|
+
"positiveStatus": "complete",
|
|
200
|
+
"positiveAllowsBlockingRisks": false,
|
|
201
|
+
"evidenceIdUnique": true,
|
|
202
|
+
"evidenceReferences": ["verification", "risks"],
|
|
203
|
+
"recordBindingReviewKind": "final-verification",
|
|
204
|
+
"recordBindingRequiredFields": ["reviewedRecord", "reviewedRecordSha256"],
|
|
205
|
+
"recordBindingForbiddenUnderNoRecord": [
|
|
206
|
+
"reviewedRecord",
|
|
207
|
+
"reviewedRecordSha256"
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SpecialistResult,
|
|
3
|
+
SpecialistRole,
|
|
4
|
+
OracleReviewKind,
|
|
5
|
+
} from '../../../src/harness/core/result-contract';
|
|
6
|
+
|
|
7
|
+
export interface ResultValidationOptions {
|
|
8
|
+
role?: SpecialistRole;
|
|
9
|
+
reviewKind?: OracleReviewKind;
|
|
10
|
+
recordSha256?: string;
|
|
11
|
+
noRecord?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type ResultIssueCode =
|
|
14
|
+
| 'ENVELOPE_FORMAT'
|
|
15
|
+
| 'JSON_SYNTAX'
|
|
16
|
+
| 'JSON_DUPLICATE_KEY'
|
|
17
|
+
| 'REQUIRED_FIELD'
|
|
18
|
+
| 'UNDECLARED_FIELD'
|
|
19
|
+
| 'TYPE_MISMATCH'
|
|
20
|
+
| 'INVALID_VALUE'
|
|
21
|
+
| 'ROLE_MISMATCH'
|
|
22
|
+
| 'REVIEW_KIND_MISMATCH'
|
|
23
|
+
| 'EVIDENCE_ID_DUPLICATE'
|
|
24
|
+
| 'EVIDENCE_REFERENCE'
|
|
25
|
+
| 'VERDICT_MISMATCH'
|
|
26
|
+
| 'VERDICT_STATUS'
|
|
27
|
+
| 'BLOCKING_RISK'
|
|
28
|
+
| 'RECORD_BINDING_REQUIRED'
|
|
29
|
+
| 'RECORD_BINDING_FORBIDDEN'
|
|
30
|
+
| 'RECORD_DIGEST_MISMATCH'
|
|
31
|
+
| 'EXPECTATION_INVALID'
|
|
32
|
+
| 'CLI_USAGE'
|
|
33
|
+
| 'CLI_INPUT';
|
|
34
|
+
export interface ResultValidationIssue {
|
|
35
|
+
code: ResultIssueCode;
|
|
36
|
+
/** JSON Pointer; the empty string denotes the whole message/envelope. */
|
|
37
|
+
path: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
export type ResultValidation =
|
|
41
|
+
| { valid: true; envelope: SpecialistResult; issues: [] }
|
|
42
|
+
| { valid: false; envelope?: never; issues: ResultValidationIssue[] };
|
|
43
|
+
export function validateResult(
|
|
44
|
+
message: string,
|
|
45
|
+
options?: ResultValidationOptions,
|
|
46
|
+
): ResultValidation;
|
|
47
|
+
export const RESULT_VALIDATOR_USAGE: string;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const contract = JSON.parse(
|
|
6
|
+
readFileSync(new URL('./result-contract.json', import.meta.url), 'utf8'),
|
|
7
|
+
);
|
|
8
|
+
const childPath = (path, key) =>
|
|
9
|
+
`${path}/${String(key).replaceAll('~', '~0').replaceAll('/', '~1')}`;
|
|
10
|
+
|
|
11
|
+
/** Parse JSON ourselves so duplicate keys cannot be erased by JSON.parse. */
|
|
12
|
+
function parseStrictJSON(source) {
|
|
13
|
+
let cursor = 0;
|
|
14
|
+
const fail = (code, path, message) => {
|
|
15
|
+
throw { code, path, message };
|
|
16
|
+
};
|
|
17
|
+
const whitespace = () => {
|
|
18
|
+
while (/[\t\n\r ]/.test(source[cursor] ?? '\0')) cursor++;
|
|
19
|
+
};
|
|
20
|
+
const syntax = (path) =>
|
|
21
|
+
fail('JSON_SYNTAX', path, `Invalid JSON at offset ${cursor}.`);
|
|
22
|
+
function string(path) {
|
|
23
|
+
const start = cursor++;
|
|
24
|
+
while (cursor < source.length) {
|
|
25
|
+
const char = source[cursor++];
|
|
26
|
+
if (char === '\\') cursor++;
|
|
27
|
+
else if (char === '"') {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(source.slice(start, cursor));
|
|
30
|
+
} catch {
|
|
31
|
+
syntax(path);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
syntax(path);
|
|
36
|
+
}
|
|
37
|
+
function value(path, depth) {
|
|
38
|
+
if (depth > 256)
|
|
39
|
+
fail('JSON_SYNTAX', path, 'JSON nesting exceeds 256 levels.');
|
|
40
|
+
whitespace();
|
|
41
|
+
const char = source[cursor];
|
|
42
|
+
if (char === '"') return string(path);
|
|
43
|
+
if (char === '{' || char === '[') {
|
|
44
|
+
const isObject = char === '{';
|
|
45
|
+
const close = isObject ? '}' : ']';
|
|
46
|
+
const result = isObject ? Object.create(null) : [];
|
|
47
|
+
const keys = new Set();
|
|
48
|
+
cursor++;
|
|
49
|
+
whitespace();
|
|
50
|
+
if (source[cursor] === close) {
|
|
51
|
+
cursor++;
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
for (;;) {
|
|
55
|
+
let key = result.length;
|
|
56
|
+
if (isObject) {
|
|
57
|
+
if (source[cursor] !== '"') syntax(path);
|
|
58
|
+
key = string(path);
|
|
59
|
+
if (keys.has(key))
|
|
60
|
+
fail(
|
|
61
|
+
'JSON_DUPLICATE_KEY',
|
|
62
|
+
childPath(path, key),
|
|
63
|
+
`Duplicate key ${JSON.stringify(key)}.`,
|
|
64
|
+
);
|
|
65
|
+
keys.add(key);
|
|
66
|
+
whitespace();
|
|
67
|
+
if (source[cursor++] !== ':') syntax(path);
|
|
68
|
+
}
|
|
69
|
+
result[key] = value(childPath(path, key), depth + 1);
|
|
70
|
+
whitespace();
|
|
71
|
+
if (source[cursor] === close) {
|
|
72
|
+
cursor++;
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
if (source[cursor++] !== ',') syntax(path);
|
|
76
|
+
whitespace();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const token =
|
|
80
|
+
/^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/.exec(
|
|
81
|
+
source.slice(cursor),
|
|
82
|
+
);
|
|
83
|
+
if (!token) syntax(path);
|
|
84
|
+
cursor += token[0].length;
|
|
85
|
+
const result = JSON.parse(token[0]);
|
|
86
|
+
if (typeof result === 'number' && !Number.isFinite(result)) syntax(path);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
const result = value('', 0);
|
|
90
|
+
whitespace();
|
|
91
|
+
if (cursor !== source.length) syntax('');
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function checkExpectations(options, issue) {
|
|
96
|
+
if (
|
|
97
|
+
options.noRecord &&
|
|
98
|
+
(options.recordSha256 !== undefined ||
|
|
99
|
+
(options.reviewKind !== undefined &&
|
|
100
|
+
options.reviewKind !== contract.rules.recordBindingReviewKind))
|
|
101
|
+
)
|
|
102
|
+
issue(
|
|
103
|
+
'EXPECTATION_INVALID',
|
|
104
|
+
'',
|
|
105
|
+
'No-record mode requires final-verification without an expected digest.',
|
|
106
|
+
);
|
|
107
|
+
const expectations = [
|
|
108
|
+
['role', Object.keys(contract.roles)],
|
|
109
|
+
['reviewKind', Object.keys(contract.reviewKinds)],
|
|
110
|
+
];
|
|
111
|
+
for (const [key, values] of expectations) {
|
|
112
|
+
if (options[key] !== undefined && !values.includes(options[key]))
|
|
113
|
+
issue('EXPECTATION_INVALID', '', `Unknown expected ${key}.`);
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
options.recordSha256 !== undefined &&
|
|
117
|
+
!/^[a-f0-9]{64}$/.test(options.recordSha256)
|
|
118
|
+
)
|
|
119
|
+
issue(
|
|
120
|
+
'EXPECTATION_INVALID',
|
|
121
|
+
'',
|
|
122
|
+
'Expected digest must be 64 lowercase hex characters.',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function validateResult(message, options = {}) {
|
|
127
|
+
const issues = [];
|
|
128
|
+
const issue = (code, path, text) =>
|
|
129
|
+
issues.push({ code, path, message: text });
|
|
130
|
+
const invalid = () => ({ valid: false, issues });
|
|
131
|
+
checkExpectations(options, issue);
|
|
132
|
+
if (issues.length) return invalid();
|
|
133
|
+
const trimmed = typeof message === 'string' ? message.trim() : '';
|
|
134
|
+
const block = new RegExp(
|
|
135
|
+
`^\`\`\`${contract.fence}\\r?\\n([\\s\\S]*?)\\r?\\n\`\`\`$`,
|
|
136
|
+
).exec(trimmed);
|
|
137
|
+
if (!block || (trimmed.match(/^```.*$/gm) ?? []).length !== 2) {
|
|
138
|
+
issue(
|
|
139
|
+
'ENVELOPE_FORMAT',
|
|
140
|
+
'',
|
|
141
|
+
'Expected exactly one thoth-result fence and no surrounding prose.',
|
|
142
|
+
);
|
|
143
|
+
return invalid();
|
|
144
|
+
}
|
|
145
|
+
let envelope;
|
|
146
|
+
try {
|
|
147
|
+
envelope = parseStrictJSON(block[1]);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
issues.push(error);
|
|
150
|
+
return invalid();
|
|
151
|
+
}
|
|
152
|
+
function check(value, schema, path) {
|
|
153
|
+
const type =
|
|
154
|
+
value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
155
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
156
|
+
if (!types.includes(type)) {
|
|
157
|
+
issue('TYPE_MISMATCH', path, `Expected ${types.join(' or ')}.`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if ('const' in schema && value !== schema.const)
|
|
161
|
+
issue('INVALID_VALUE', path, 'Value differs from required constant.');
|
|
162
|
+
if (schema.enum && !schema.enum.includes(value))
|
|
163
|
+
issue('INVALID_VALUE', path, 'Value is not in the allowed enumeration.');
|
|
164
|
+
if (type === 'string') {
|
|
165
|
+
if (schema.minLength && value.length < schema.minLength)
|
|
166
|
+
issue('INVALID_VALUE', path, 'String must not be empty.');
|
|
167
|
+
if (schema.pattern && !new RegExp(schema.pattern).test(value))
|
|
168
|
+
issue(
|
|
169
|
+
'INVALID_VALUE',
|
|
170
|
+
path,
|
|
171
|
+
'String does not match the required pattern.',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (type === 'array') {
|
|
175
|
+
value.forEach((item, index) => {
|
|
176
|
+
check(item, schema.items, childPath(path, index));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (type === 'object') {
|
|
180
|
+
for (const key of schema.required)
|
|
181
|
+
if (!Object.hasOwn(value, key))
|
|
182
|
+
issue(
|
|
183
|
+
'REQUIRED_FIELD',
|
|
184
|
+
childPath(path, key),
|
|
185
|
+
'Required field is missing.',
|
|
186
|
+
);
|
|
187
|
+
for (const key of Object.keys(value)) {
|
|
188
|
+
if (!Object.hasOwn(schema.properties, key))
|
|
189
|
+
issue(
|
|
190
|
+
'UNDECLARED_FIELD',
|
|
191
|
+
childPath(path, key),
|
|
192
|
+
'Undeclared field is forbidden.',
|
|
193
|
+
);
|
|
194
|
+
else check(value[key], schema.properties[key], childPath(path, key));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const roleSchema =
|
|
199
|
+
envelope &&
|
|
200
|
+
typeof envelope.role === 'string' &&
|
|
201
|
+
Object.hasOwn(contract.roles, envelope.role)
|
|
202
|
+
? contract.roles[envelope.role]
|
|
203
|
+
: { properties: {}, required: [] };
|
|
204
|
+
check(
|
|
205
|
+
envelope,
|
|
206
|
+
{
|
|
207
|
+
...contract.common,
|
|
208
|
+
properties: { ...contract.common.properties, ...roleSchema.properties },
|
|
209
|
+
required: [...contract.common.required, ...roleSchema.required],
|
|
210
|
+
},
|
|
211
|
+
'',
|
|
212
|
+
);
|
|
213
|
+
if (issues.length) return invalid();
|
|
214
|
+
if (
|
|
215
|
+
options.noRecord &&
|
|
216
|
+
envelope.review.kind !== contract.rules.recordBindingReviewKind
|
|
217
|
+
) {
|
|
218
|
+
issue(
|
|
219
|
+
'EXPECTATION_INVALID',
|
|
220
|
+
'/review/kind',
|
|
221
|
+
'No-record mode requires final-verification.',
|
|
222
|
+
);
|
|
223
|
+
return invalid();
|
|
224
|
+
}
|
|
225
|
+
if (options.role && envelope.role !== options.role)
|
|
226
|
+
issue('ROLE_MISMATCH', '/role', 'Role does not match assignment.');
|
|
227
|
+
if (options.reviewKind && envelope.review.kind !== options.reviewKind)
|
|
228
|
+
issue(
|
|
229
|
+
'REVIEW_KIND_MISMATCH',
|
|
230
|
+
'/review/kind',
|
|
231
|
+
'Review kind does not match assignment.',
|
|
232
|
+
);
|
|
233
|
+
const ids = new Set();
|
|
234
|
+
envelope.evidence.forEach((entry, index) => {
|
|
235
|
+
if (contract.rules.evidenceIdUnique && ids.has(entry.id))
|
|
236
|
+
issue(
|
|
237
|
+
'EVIDENCE_ID_DUPLICATE',
|
|
238
|
+
`/evidence/${index}/id`,
|
|
239
|
+
'Evidence ids must be unique.',
|
|
240
|
+
);
|
|
241
|
+
ids.add(entry.id);
|
|
242
|
+
});
|
|
243
|
+
for (const field of contract.rules.evidenceReferences) {
|
|
244
|
+
envelope[field].forEach((entry, index) => {
|
|
245
|
+
entry.evidenceIds.forEach((id, ref) => {
|
|
246
|
+
if (!ids.has(id))
|
|
247
|
+
issue(
|
|
248
|
+
'EVIDENCE_REFERENCE',
|
|
249
|
+
`/${field}/${index}/evidenceIds/${ref}`,
|
|
250
|
+
'Reference does not identify supplied evidence.',
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const { kind, verdict } = envelope.review;
|
|
256
|
+
if (!contract.reviewKinds[kind].verdicts.includes(verdict))
|
|
257
|
+
issue(
|
|
258
|
+
'VERDICT_MISMATCH',
|
|
259
|
+
'/review/verdict',
|
|
260
|
+
'Verdict is not allowed for this review kind.',
|
|
261
|
+
);
|
|
262
|
+
if (contract.rules.positiveVerdicts.includes(verdict)) {
|
|
263
|
+
if (envelope.status !== contract.rules.positiveStatus)
|
|
264
|
+
issue(
|
|
265
|
+
'VERDICT_STATUS',
|
|
266
|
+
'/status',
|
|
267
|
+
'Positive verdict requires complete status.',
|
|
268
|
+
);
|
|
269
|
+
envelope.risks.forEach((risk, index) => {
|
|
270
|
+
if (!contract.rules.positiveAllowsBlockingRisks && risk.blocking)
|
|
271
|
+
issue(
|
|
272
|
+
'BLOCKING_RISK',
|
|
273
|
+
`/risks/${index}/blocking`,
|
|
274
|
+
'Positive verdict forbids blocking risks.',
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (options.noRecord) {
|
|
279
|
+
for (const key of contract.rules.recordBindingForbiddenUnderNoRecord) {
|
|
280
|
+
if (Object.hasOwn(envelope.review, key))
|
|
281
|
+
issue(
|
|
282
|
+
'RECORD_BINDING_FORBIDDEN',
|
|
283
|
+
`/review/${key}`,
|
|
284
|
+
'No-record mode forbids reviewed record binding.',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
} else if (envelope.review.kind === contract.rules.recordBindingReviewKind) {
|
|
288
|
+
for (const key of contract.rules.recordBindingRequiredFields) {
|
|
289
|
+
if (!envelope.review[key])
|
|
290
|
+
issue(
|
|
291
|
+
'RECORD_BINDING_REQUIRED',
|
|
292
|
+
`/review/${key}`,
|
|
293
|
+
'Final verification requires reviewed record binding.',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (options.recordSha256 !== undefined) {
|
|
298
|
+
if (envelope.review.kind !== contract.rules.recordBindingReviewKind)
|
|
299
|
+
issue(
|
|
300
|
+
'REVIEW_KIND_MISMATCH',
|
|
301
|
+
'/review/kind',
|
|
302
|
+
'Record binding requires final-verification.',
|
|
303
|
+
);
|
|
304
|
+
if (
|
|
305
|
+
envelope.review.reviewedRecordSha256 &&
|
|
306
|
+
envelope.review.reviewedRecordSha256 !== options.recordSha256
|
|
307
|
+
)
|
|
308
|
+
issue(
|
|
309
|
+
'RECORD_DIGEST_MISMATCH',
|
|
310
|
+
'/review/reviewedRecordSha256',
|
|
311
|
+
'Digest does not match assignment.',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return issues.length ? invalid() : { valid: true, envelope, issues };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export const RESULT_VALIDATOR_USAGE =
|
|
318
|
+
'node validate-result.mjs [--role oracle] [--review-kind plan-review|final-verification] [--record-sha256 <hex>] [--no-record] [--input <path>]';
|
|
319
|
+
function main(args) {
|
|
320
|
+
const options = {};
|
|
321
|
+
let input;
|
|
322
|
+
const names = {
|
|
323
|
+
'--role': 'role',
|
|
324
|
+
'--review-kind': 'reviewKind',
|
|
325
|
+
'--record-sha256': 'recordSha256',
|
|
326
|
+
'--no-record': 'noRecord',
|
|
327
|
+
'--input': 'input',
|
|
328
|
+
};
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
for (let index = 0; index < args.length; index++) {
|
|
331
|
+
const flag = args[index];
|
|
332
|
+
if (
|
|
333
|
+
!Object.hasOwn(names, flag) ||
|
|
334
|
+
seen.has(flag) ||
|
|
335
|
+
(flag !== '--no-record' &&
|
|
336
|
+
(!args[index + 1] || args[index + 1].startsWith('--')))
|
|
337
|
+
) {
|
|
338
|
+
console.log(
|
|
339
|
+
JSON.stringify({
|
|
340
|
+
valid: false,
|
|
341
|
+
issues: [
|
|
342
|
+
{ code: 'CLI_USAGE', path: '', message: RESULT_VALIDATOR_USAGE },
|
|
343
|
+
],
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
return 2;
|
|
347
|
+
}
|
|
348
|
+
seen.add(flag);
|
|
349
|
+
if (flag === '--no-record') {
|
|
350
|
+
options.noRecord = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const value = args[++index];
|
|
354
|
+
if (flag === '--input') input = value;
|
|
355
|
+
else options[names[flag]] = value;
|
|
356
|
+
}
|
|
357
|
+
const expectationIssues = [];
|
|
358
|
+
checkExpectations(options, (code, path, message) => {
|
|
359
|
+
expectationIssues.push({ code, path, message });
|
|
360
|
+
});
|
|
361
|
+
if (expectationIssues.length) {
|
|
362
|
+
console.log(JSON.stringify({ valid: false, issues: expectationIssues }));
|
|
363
|
+
return 2;
|
|
364
|
+
}
|
|
365
|
+
let message;
|
|
366
|
+
try {
|
|
367
|
+
message = readFileSync(input ?? 0, 'utf8');
|
|
368
|
+
} catch {
|
|
369
|
+
console.log(
|
|
370
|
+
JSON.stringify({
|
|
371
|
+
valid: false,
|
|
372
|
+
issues: [
|
|
373
|
+
{ code: 'CLI_INPUT', path: '', message: 'Unable to read input.' },
|
|
374
|
+
],
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
return 2;
|
|
378
|
+
}
|
|
379
|
+
const result = validateResult(message, options);
|
|
380
|
+
console.log(JSON.stringify(result));
|
|
381
|
+
if (result.valid) return 0;
|
|
382
|
+
return result.issues.some((issue) => issue.code === 'EXPECTATION_INVALID')
|
|
383
|
+
? 2
|
|
384
|
+
: 1;
|
|
385
|
+
}
|
|
386
|
+
if (
|
|
387
|
+
process.argv[1] &&
|
|
388
|
+
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
389
|
+
)
|
|
390
|
+
process.exitCode = main(process.argv.slice(2));
|