orchestrator-workflow 0.35.0 → 0.36.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/CHANGELOG.md +55 -0
- package/README.md +38 -0
- package/assets/agents/implementer.md +13 -0
- package/assets/skill/references/contracts.md +18 -0
- package/dist/cli.js +95 -0
- package/dist/review-report.d.ts +157 -0
- package/dist/review-report.js +449 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.36.0] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
- A `validate-review-report <file>` CLI subcommand (`-` reads stdin) checks a
|
|
13
|
+
reviewer return's YAML against the reviewer output contract's required
|
|
14
|
+
fields and enums, printing one diagnostic per missing or invalid field and
|
|
15
|
+
exiting `0`/`1`/`2` for valid/structurally-invalid/usage-error; `--format
|
|
16
|
+
json` prints the same diagnostics as one JSON object. The check is
|
|
17
|
+
structural only, never semantic, and never constitutes orchestrator
|
|
18
|
+
acceptance. Its schema lives in `src/review-report.ts` and is pinned in
|
|
19
|
+
`test/docs-consistency.test.ts` against the contract block in
|
|
20
|
+
`assets/agents/reviewer.md` itself, so a contract edit without a matching
|
|
21
|
+
schema edit fails the suite instead of drifting silently. An excess
|
|
22
|
+
positional argument (e.g. two file paths) now also exits `2` as a usage
|
|
23
|
+
error instead of silently validating only the first path. A fenced
|
|
24
|
+
return ends at the first closing fence that starts at column 0, so a
|
|
25
|
+
triple-backtick sequence inside a value (a reviewer quoting a fenced
|
|
26
|
+
snippet in a `description`) no longer closes the block early and hands
|
|
27
|
+
the parser a truncated document.
|
|
28
|
+
- Removed incidental blank-line padding (a run of seven consecutive blank
|
|
29
|
+
lines) and a mid-sentence paragraph split from
|
|
30
|
+
`docs/okf/subagent-contracts-superset.md`'s mutation-probe field
|
|
31
|
+
discussion, and corrected its Commits field section, which still said
|
|
32
|
+
the not-applicable `commits: []` clause is pinned "in both copies"
|
|
33
|
+
after the 0.35.0 contract-reduction refactor left it in the installed
|
|
34
|
+
implementer prompt alone. Re-pointed every citation the removed lines
|
|
35
|
+
shifted (two `docs/okf/log.md` self-citations into this doc, and the
|
|
36
|
+
`SIBLING_GUARD_BUNDLE_ALLOWLIST` explanation comment in
|
|
37
|
+
`test/docs-consistency.test.ts`, whose recorded `:1217`/`:1232`
|
|
38
|
+
coordinates no longer matched the paragraph's current citations).
|
|
39
|
+
Docs-only: no YAML contract, role prompt, guard matcher, or exemption
|
|
40
|
+
geometry changed. Anchored by agent-dx tracker task 8a55e082.
|
|
41
|
+
|
|
42
|
+
- `assets/agents/implementer.md` gains a pre-return rule bullet next to
|
|
43
|
+
the commit-reporting ones: before committing, when slop-detector is
|
|
44
|
+
available run `slop-detector check <changed file> [<changed file> ...]
|
|
45
|
+
--pack review-slop` over every changed file and `git log -1
|
|
46
|
+
--format=%B | slop-detector check --stdin-path COMMIT_MSG --pack
|
|
47
|
+
review-slop` over the commit message, with the repository-vendored
|
|
48
|
+
`node packages/slop-detector/dist/cli.js check ...` path named as the
|
|
49
|
+
alternative where the CLI is not installed on PATH; fix every
|
|
50
|
+
block-level finding before returning, or add a legitimate match to
|
|
51
|
+
`review.allow`. Only exit `0` or `1` is a result: exit `2` is a usage
|
|
52
|
+
error, not a clean check. A returned report that skipped the check on a
|
|
53
|
+
diff with block-level findings is a misfire, not evidence. Pinned by
|
|
54
|
+
`test/docs-consistency.test.ts`;
|
|
55
|
+
`docs/okf/subagent-contracts-superset.md` gained a matching sentence
|
|
56
|
+
describing the rule.
|
|
57
|
+
- Anchored by pandora batch 51 (`.ai/runs/2026-09-13-quickwins-batch51`):
|
|
58
|
+
four review rounds (or post-merge cleanup commits) across five repos
|
|
59
|
+
were spent catching run-local review tokens (finding ids, round
|
|
60
|
+
references, workspace-handoff phrases) that nothing mechanical
|
|
61
|
+
flagged before `slop-detector`'s new `review-slop` pack existed; see
|
|
62
|
+
that package's own CHANGELOG.md for the pack itself and the
|
|
63
|
+
pre-cleanup PR the fixtures were drawn from.
|
|
64
|
+
|
|
10
65
|
## [0.35.0] - 2026-09-15
|
|
11
66
|
|
|
12
67
|
- The npm tarball now ships a `LICENSE` file matching the repo root LICENSE
|
package/README.md
CHANGED
|
@@ -691,3 +691,41 @@ references.
|
|
|
691
691
|
package's version, so a release of `okf-kit` must bump those pins in the
|
|
692
692
|
same commit as the version cut; see `CONTRIBUTING.md`'s "Releasing okf-kit"
|
|
693
693
|
section (repo root) for the order.
|
|
694
|
+
|
|
695
|
+
## Reviewer-report validation
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
orchestrator-workflow validate-review-report path/to/return.yaml
|
|
699
|
+
orchestrator-workflow validate-review-report - < path/to/return.yaml
|
|
700
|
+
orchestrator-workflow validate-review-report path/to/return.yaml --format json
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Checks a reviewer return's YAML against the reviewer output contract's
|
|
704
|
+
required fields and enums (see the "Reviewer output contract" section of
|
|
705
|
+
`assets/skill/references/contracts.md`, byte-identical to the contract in
|
|
706
|
+
`assets/agents/reviewer.md`), whether the return is fenced in a code
|
|
707
|
+
block (any language tag, or none) or given unfenced, and prints one
|
|
708
|
+
diagnostic per missing or invalid field. A fenced return ends at the
|
|
709
|
+
first closing fence that starts at column 0, so a reviewer quoting a
|
|
710
|
+
fenced snippet inside a value (a `description` block scalar, which YAML
|
|
711
|
+
indents) does not truncate the return. `--format json` prints the same
|
|
712
|
+
diagnostics as a single JSON object instead of human-readable text. It
|
|
713
|
+
exits `0` when the return is structurally valid, `1` when it is
|
|
714
|
+
structurally invalid (a required field is missing or its value falls
|
|
715
|
+
outside its enum, or the input is unparsable, empty, or not a mapping),
|
|
716
|
+
and `2` for a usage error (an unreadable file, an unrecognized `--format`
|
|
717
|
+
value, a missing `<file>` argument, an unknown option, or an excess
|
|
718
|
+
positional argument).
|
|
719
|
+
`--format json` governs the validation verdict only: a commander parsing
|
|
720
|
+
error (missing argument, unknown option, excess arguments) or an
|
|
721
|
+
unrecognized `--format` value itself still prints plain text to stderr
|
|
722
|
+
with nothing on stdout, regardless of `--format`; the one exception is an
|
|
723
|
+
unreadable file, which does emit the JSON envelope on stdout. This check
|
|
724
|
+
is structural only: it never judges semantic adequacy, cannot waive a
|
|
725
|
+
finding, and passing it is never orchestrator acceptance. The
|
|
726
|
+
required-field set it checks is hand-maintained in `src/review-report.ts`
|
|
727
|
+
and pinned against the contract block itself by
|
|
728
|
+
`test/docs-consistency.test.ts`, so a contract edit without a matching
|
|
729
|
+
schema edit fails the suite instead of drifting silently; every field
|
|
730
|
+
listed there is dispatched to its own checker, so an entry added to the
|
|
731
|
+
list without a checker fails to typecheck rather than passing unchecked.
|
|
@@ -133,6 +133,19 @@ Rules:
|
|
|
133
133
|
than omitting the field.
|
|
134
134
|
- Populate a non-empty `commits` field by pasting `git log --reverse
|
|
135
135
|
--format=%H <base>..HEAD`; never type or hand-complete commit shas.
|
|
136
|
+
- Before committing, when slop-detector is available run `slop-detector
|
|
137
|
+
check <changed file> [<changed file> ...] --pack review-slop` over every
|
|
138
|
+
changed file, and `git log -1 --format=%B | slop-detector check
|
|
139
|
+
--stdin-path COMMIT_MSG --pack review-slop` over the commit message;
|
|
140
|
+
where it is vendored in the repository rather than installed on PATH,
|
|
141
|
+
the same two invocations run as `node
|
|
142
|
+
packages/slop-detector/dist/cli.js check ...`. Fix every block-level
|
|
143
|
+
finding before returning, or add a legitimate match to `review.allow`
|
|
144
|
+
in the repository's slop.config.yml rather than deleting correct text.
|
|
145
|
+
Only exit `0` or `1` is a result; exit `2` is a usage error (a mistyped
|
|
146
|
+
invocation, or `--stdin-path` with nothing piped in), so it is not a
|
|
147
|
+
clean check. A returned report that skipped this check on a diff with
|
|
148
|
+
block-level findings is a misfire, not evidence.
|
|
136
149
|
- Verification plans, probe plans, and repeat tallies run in the foreground,
|
|
137
150
|
and the implementer reports their returns in the same turn as the last
|
|
138
151
|
check. A background monitor is no substitute for those returns.
|
|
@@ -165,6 +165,24 @@ withdrawn:
|
|
|
165
165
|
When it is missing, the orchestrator asks the reviewer to resupply it
|
|
166
166
|
instead of inferring one from the findings list.
|
|
167
167
|
|
|
168
|
+
A structural check for this exact contract ships as a CLI subcommand:
|
|
169
|
+
`orchestrator-workflow validate-review-report <file>` (pass `-` to read the
|
|
170
|
+
return from stdin instead) parses the reviewer return's YAML, fenced in a
|
|
171
|
+
code block with any language tag or none, or unfenced, and checks the
|
|
172
|
+
required fields and enums above one by one, printing a diagnostic (path,
|
|
173
|
+
expected, got) for each missing or invalid field; add `--format json` for
|
|
174
|
+
the same diagnostics as a single JSON object. It exits `0` when the return
|
|
175
|
+
is structurally valid, `1` when it is structurally invalid (a missing or
|
|
176
|
+
out-of-enum required field, or unparsable, empty, non-mapping input), and
|
|
177
|
+
`2` for a usage error (an unreadable file, an unrecognized `--format`
|
|
178
|
+
value, or an argument-parsing error: a missing `<file>` argument, an
|
|
179
|
+
unknown option, an excess positional argument). `--format json` governs
|
|
180
|
+
the validation verdict only: an argument-parsing error or an unrecognized
|
|
181
|
+
`--format` value still prints plain text to stderr, except an unreadable
|
|
182
|
+
file, which still emits the JSON envelope on stdout. The check is
|
|
183
|
+
structural only: it never judges semantic adequacy, cannot waive a
|
|
184
|
+
finding, and passing it is never orchestrator acceptance.
|
|
185
|
+
|
|
168
186
|
`recurrence` classifies each finding against earlier rounds on the same
|
|
169
187
|
task: `new` for a defect class not previously found here, `repeated` for
|
|
170
188
|
one that already appeared in an earlier round. On a task's first review
|
package/dist/cli.js
CHANGED
|
@@ -1172,6 +1172,101 @@ program
|
|
|
1172
1172
|
printTargetDetail(targetReport, PACKAGE_VERSION);
|
|
1173
1173
|
process.exitCode = exitCode;
|
|
1174
1174
|
});
|
|
1175
|
+
const validateReviewReportCommand = program
|
|
1176
|
+
.command("validate-review-report")
|
|
1177
|
+
.description("Check a reviewer return's YAML against the reviewer output contract's required fields and enums; reports structural validity ONLY, never semantic adequacy, and never waives a finding or constitutes orchestrator acceptance")
|
|
1178
|
+
.argument("<file>", "path to a file holding the reviewer return's YAML, or - to read stdin")
|
|
1179
|
+
.option("--format <format>", "output format: text (default) or json", "text")
|
|
1180
|
+
// commander 12's default for a subcommand is `allowExcessArguments:
|
|
1181
|
+
// true`, so `validate-review-report a.yaml extra.yaml` silently
|
|
1182
|
+
// validated only `a.yaml` and exited 0 (fix-round, review finding M1).
|
|
1183
|
+
// Disabling it turns a trailing extra argument into commander's own
|
|
1184
|
+
// "too many arguments" parsing error, which the scoped `exitOverride`
|
|
1185
|
+
// below then maps to exit 2 like every other usage error.
|
|
1186
|
+
.allowExcessArguments(false)
|
|
1187
|
+
.action(async (file, opts) => {
|
|
1188
|
+
// Imported dynamically, here rather than as a top-level static import,
|
|
1189
|
+
// so this command's addition cannot shift the line numbers of any
|
|
1190
|
+
// statement above it in this file: several docs/okf/ citations anchor
|
|
1191
|
+
// to exact lines in src/cli.ts, and a top-level import would have
|
|
1192
|
+
// re-pointed all of them for a reason unrelated to their own content.
|
|
1193
|
+
const { STRUCTURAL_ONLY_NOTE, validateReviewReport } = await import("./review-report.js");
|
|
1194
|
+
const format = opts.format ?? "text";
|
|
1195
|
+
if (format !== "text" && format !== "json") {
|
|
1196
|
+
console.error(`Unknown --format value: ${format} (expected "text" or "json")`);
|
|
1197
|
+
console.error(STRUCTURAL_ONLY_NOTE);
|
|
1198
|
+
process.exitCode = 2;
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
let raw;
|
|
1202
|
+
try {
|
|
1203
|
+
raw = readFileSync(file === "-" ? 0 : file, "utf8");
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1207
|
+
const label = file === "-" ? "stdin" : file;
|
|
1208
|
+
if (format === "json") {
|
|
1209
|
+
console.log(JSON.stringify({
|
|
1210
|
+
valid: false,
|
|
1211
|
+
diagnostics: [
|
|
1212
|
+
{ path: "<file>", expected: "a readable file", got: message },
|
|
1213
|
+
],
|
|
1214
|
+
warnings: [],
|
|
1215
|
+
note: STRUCTURAL_ONLY_NOTE,
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
console.error(`Could not read ${label}: ${message}`);
|
|
1220
|
+
console.error(STRUCTURAL_ONLY_NOTE);
|
|
1221
|
+
}
|
|
1222
|
+
process.exitCode = 2;
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const result = validateReviewReport(raw);
|
|
1226
|
+
if (format === "json") {
|
|
1227
|
+
console.log(JSON.stringify({
|
|
1228
|
+
valid: result.valid,
|
|
1229
|
+
diagnostics: result.diagnostics,
|
|
1230
|
+
warnings: result.warnings,
|
|
1231
|
+
note: STRUCTURAL_ONLY_NOTE,
|
|
1232
|
+
}));
|
|
1233
|
+
process.exitCode = result.valid ? 0 : 1;
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
for (const warning of result.warnings) {
|
|
1237
|
+
console.error(`warning: ${warning}`);
|
|
1238
|
+
}
|
|
1239
|
+
// Both the exit-0 (valid) and exit-1 (invalid) cases are "verdict
|
|
1240
|
+
// paths": a validation verdict was actually computed, so its message
|
|
1241
|
+
// and the structural-only note that qualifies it belong on the same
|
|
1242
|
+
// stream. Previously the note printed on stderr unconditionally while
|
|
1243
|
+
// the valid-case verdict printed on stdout, splitting one reading
|
|
1244
|
+
// across two streams (fix-round, review finding L5); the two exit-2
|
|
1245
|
+
// "usage error" branches above keep stderr for both, since no verdict
|
|
1246
|
+
// was computed there.
|
|
1247
|
+
if (result.valid) {
|
|
1248
|
+
console.log("Structurally valid reviewer return.");
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
console.log(`Structurally invalid reviewer return (${result.diagnostics.length} issue${result.diagnostics.length === 1 ? "" : "s"}):`);
|
|
1252
|
+
for (const diagnostic of result.diagnostics) {
|
|
1253
|
+
console.log(` ${diagnostic.path}: expected ${diagnostic.expected}, got ${diagnostic.got}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
console.log(STRUCTURAL_ONLY_NOTE);
|
|
1257
|
+
process.exitCode = result.valid ? 0 : 1;
|
|
1258
|
+
});
|
|
1259
|
+
// Commander's default for a parsing failure (a missing `<file>` argument,
|
|
1260
|
+
// an unknown option, excess arguments) is exit code 1, the same code this
|
|
1261
|
+
// command otherwise reserves for "structurally invalid" -- collapsing
|
|
1262
|
+
// "you didn't invoke this right" into "the return you gave me is invalid"
|
|
1263
|
+
// (fix-round, review finding L1). Scoped to this one subcommand so every
|
|
1264
|
+
// other command's existing commander-parsing exit behavior is untouched:
|
|
1265
|
+
// commander already prints the error message itself before calling this
|
|
1266
|
+
// callback, so remapping the exit code is all that is needed here.
|
|
1267
|
+
validateReviewReportCommand.exitOverride((err) => {
|
|
1268
|
+
process.exit(err.exitCode === 0 ? 0 : 2);
|
|
1269
|
+
});
|
|
1175
1270
|
program.parseAsync(process.argv).catch((error) => {
|
|
1176
1271
|
console.error(error instanceof Error ? error.message : error);
|
|
1177
1272
|
process.exitCode = 1;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural schema for the reviewer output contract (the final ```yaml
|
|
3
|
+
* block in `assets/agents/reviewer.md`, mirrored byte-for-byte in
|
|
4
|
+
* `assets/skill/references/contracts.md`). This module is the single
|
|
5
|
+
* hand-maintained copy of that shape in code; `test/docs-consistency.test.ts`
|
|
6
|
+
* parses the contract block itself and asserts it matches the constants
|
|
7
|
+
* below exactly, so a contract edit without a matching edit here fails the
|
|
8
|
+
* suite instead of drifting silently.
|
|
9
|
+
*
|
|
10
|
+
* This validator checks structure only. It never judges semantic adequacy,
|
|
11
|
+
* never waives a finding, and its passing is never orchestrator acceptance.
|
|
12
|
+
*/
|
|
13
|
+
/** Top-level field names, in the contract's own order. */
|
|
14
|
+
export declare const TOP_LEVEL_FIELDS: readonly ["status", "role", "task_id", "summary", "findings", "acceptance_recommendation", "missing_tests", "residual_risks", "reproduction", "method_applied", "withdrawn"];
|
|
15
|
+
/** Field names of one `findings[]` entry, in the contract's own order. */
|
|
16
|
+
export declare const FINDING_FIELDS: readonly ["severity", "category", "description", "suggested_fix", "recurrence", "introduced_by_delta"];
|
|
17
|
+
/** Field names of the `reproduction` object, in the contract's own order. */
|
|
18
|
+
export declare const REPRODUCTION_FIELDS: readonly ["method", "sample_size", "result", "matches_implementer_claim"];
|
|
19
|
+
/** Field names of one `withdrawn[]` entry, in the contract's own order. */
|
|
20
|
+
export declare const WITHDRAWN_FIELDS: readonly ["description", "reason"];
|
|
21
|
+
/**
|
|
22
|
+
* Enum spellings keyed by their bare field name. Every enum-bearing field
|
|
23
|
+
* in the contract has a name unique across the whole block, so a flat map
|
|
24
|
+
* (rather than one keyed by full path) is enough and matches how the
|
|
25
|
+
* contract text itself reads (`key: a | b | c`).
|
|
26
|
+
*/
|
|
27
|
+
export declare const ENUM_VALUES: Readonly<Record<string, readonly string[]>>;
|
|
28
|
+
export interface Diagnostic {
|
|
29
|
+
/** Dotted/bracketed location of the offending field, e.g. `findings[0].severity`. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** What the schema requires at that path. */
|
|
32
|
+
expected: string;
|
|
33
|
+
/** What was actually found (or "missing"). */
|
|
34
|
+
got: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ValidationResult {
|
|
37
|
+
valid: boolean;
|
|
38
|
+
diagnostics: Diagnostic[];
|
|
39
|
+
/** Non-fatal observations, e.g. prose found after the fenced block. */
|
|
40
|
+
warnings: string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* This validator reports structural validity only. It does not judge
|
|
44
|
+
* semantic adequacy, cannot waive findings, and passing it does not
|
|
45
|
+
* constitute orchestrator acceptance.
|
|
46
|
+
*/
|
|
47
|
+
export declare const STRUCTURAL_ONLY_NOTE = "Structural check only: this does not judge semantic adequacy, cannot waive findings, and does not constitute orchestrator acceptance.";
|
|
48
|
+
/** Every field name any of the four contract constants declares. */
|
|
49
|
+
export type SchemaFieldName = (typeof TOP_LEVEL_FIELDS)[number] | (typeof FINDING_FIELDS)[number] | (typeof REPRODUCTION_FIELDS)[number] | (typeof WITHDRAWN_FIELDS)[number];
|
|
50
|
+
/**
|
|
51
|
+
* The structural kind of one contract field: which input classes its
|
|
52
|
+
* checker accepts and which it rejects. Declared once, next to the
|
|
53
|
+
* dispatch tables above, and read by both this module (for the `expected`
|
|
54
|
+
* text a diagnostic carries) and `test/review-report.test.ts`'s case
|
|
55
|
+
* generator, which derives its whole case list from these kinds rather
|
|
56
|
+
* than from a hand-enumerated list of checks.
|
|
57
|
+
*
|
|
58
|
+
* - `enum`: present, a string, and one of `ENUM_VALUES[field]`.
|
|
59
|
+
* - `string`: present and a string; the empty string is accepted.
|
|
60
|
+
* - `non-empty-string`: present, a string, and not blank.
|
|
61
|
+
* - `scalar`: present and either a string or a number.
|
|
62
|
+
* - `array`: present and an array; an empty array is accepted.
|
|
63
|
+
* - `mapping-list`: `array`, and every element a mapping.
|
|
64
|
+
* - `mapping`: present and a mapping.
|
|
65
|
+
*
|
|
66
|
+
* A new kind is declared here, in {@link KIND_EXPECTED}, and in the
|
|
67
|
+
* generator's own `Record<FieldKind, ...>` value tables; each of those is
|
|
68
|
+
* keyed by this type, so a kind with no expectation text or no input
|
|
69
|
+
* classes is a compile error rather than an untested kind.
|
|
70
|
+
*/
|
|
71
|
+
export type FieldKind = "enum" | "string" | "non-empty-string" | "scalar" | "array" | "mapping-list" | "mapping";
|
|
72
|
+
/**
|
|
73
|
+
* The declared kind of every field of every contract constant, keyed by
|
|
74
|
+
* bare field name the way {@link ENUM_VALUES} is: a name is unique across
|
|
75
|
+
* the whole contract block except for `description`, which `findings[]`
|
|
76
|
+
* and `withdrawn[]` share with the same kind. The `satisfies
|
|
77
|
+
* Record<SchemaFieldName, FieldKind>` clause means a name added to any of
|
|
78
|
+
* the four constants without a kind here is a TypeScript compile error,
|
|
79
|
+
* the same way it is already an error to add one without a checker in the
|
|
80
|
+
* dispatch tables above. Were a future contract to reuse one name at two
|
|
81
|
+
* levels with two different kinds, this flat map could hold only one of
|
|
82
|
+
* them: the generator asserts each field's real diagnostics against its
|
|
83
|
+
* declared kind, so that shows up as a failing generated case rather than
|
|
84
|
+
* as an unchecked field.
|
|
85
|
+
*/
|
|
86
|
+
export declare const FIELD_KINDS: {
|
|
87
|
+
status: "enum";
|
|
88
|
+
role: "enum";
|
|
89
|
+
task_id: "non-empty-string";
|
|
90
|
+
summary: "array";
|
|
91
|
+
findings: "mapping-list";
|
|
92
|
+
acceptance_recommendation: "enum";
|
|
93
|
+
missing_tests: "array";
|
|
94
|
+
residual_risks: "array";
|
|
95
|
+
reproduction: "mapping";
|
|
96
|
+
method_applied: "enum";
|
|
97
|
+
withdrawn: "mapping-list";
|
|
98
|
+
severity: "enum";
|
|
99
|
+
category: "enum";
|
|
100
|
+
description: "string";
|
|
101
|
+
suggested_fix: "string";
|
|
102
|
+
recurrence: "enum";
|
|
103
|
+
introduced_by_delta: "enum";
|
|
104
|
+
method: "scalar";
|
|
105
|
+
sample_size: "scalar";
|
|
106
|
+
result: "scalar";
|
|
107
|
+
matches_implementer_claim: "enum";
|
|
108
|
+
reason: "string";
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* The `expected` text this validator's diagnostics about `field` carry.
|
|
112
|
+
* The checkers above hold that text literally, at their own push sites;
|
|
113
|
+
* this is the schema's declaration of the same text, which the generated
|
|
114
|
+
* test cases assert the checkers actually produce, so a checker whose
|
|
115
|
+
* behaviour stops matching its declared kind fails a case instead of
|
|
116
|
+
* drifting quietly.
|
|
117
|
+
*/
|
|
118
|
+
export declare function expectedTextFor(field: SchemaFieldName): string;
|
|
119
|
+
/** The `expected` text a diagnostic about one element of a `mapping-list` carries. */
|
|
120
|
+
export declare const MAPPING_LIST_ELEMENT_EXPECTED: string;
|
|
121
|
+
interface ExtractedYaml {
|
|
122
|
+
yamlText: string;
|
|
123
|
+
warnings: string[];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* A reviewer return is commonly wrapped in a single fenced code block.
|
|
127
|
+
* Strips one leading/trailing fence when present, whatever language tag
|
|
128
|
+
* it carries (```yaml, ```yml, a bare ``` , or any other tag -- a
|
|
129
|
+
* language-less fence previously fell through to the "literal YAML"
|
|
130
|
+
* branch below and produced a confusing YAML parse error instead of a
|
|
131
|
+
* clean structural diagnostic; fix-round, review finding L2); anything
|
|
132
|
+
* unfenced is treated as literal YAML. The fence is located anywhere in
|
|
133
|
+
* the input, not only at its very start: a return prefixed with prose
|
|
134
|
+
* ("Here is my report:\n```yaml ...") previously fell through to the
|
|
135
|
+
* "literal YAML" branch, since the fence pattern was anchored to the
|
|
136
|
+
* start of the string, and produced a raw parse error instead of a
|
|
137
|
+
* structural diagnostic (fix-round, review finding L1). Prose found
|
|
138
|
+
* before the opening fence or after the closing fence is tolerated, but
|
|
139
|
+
* each is named as its own warning rather than silently dropped.
|
|
140
|
+
*
|
|
141
|
+
* The closing fence must start at column 0: the pattern anchors it with
|
|
142
|
+
* `^` under the `m` flag, so a triple-backtick sequence inside a value
|
|
143
|
+
* (a reviewer quoting a fenced snippet in a `description` block scalar,
|
|
144
|
+
* which YAML necessarily indents) can no longer close the block early
|
|
145
|
+
* and hand the parser a truncated document, which surfaced as
|
|
146
|
+
* diagnostics about fields the return actually carried (fix-round,
|
|
147
|
+
* review finding L3).
|
|
148
|
+
*/
|
|
149
|
+
export declare function extractYamlSource(raw: string): ExtractedYaml;
|
|
150
|
+
/**
|
|
151
|
+
* Validates a reviewer return against the reviewer output contract's
|
|
152
|
+
* structure. Reports structural validity ONLY: it never judges semantic
|
|
153
|
+
* adequacy, never waives a finding, and passing it is never orchestrator
|
|
154
|
+
* acceptance.
|
|
155
|
+
*/
|
|
156
|
+
export declare function validateReviewReport(raw: string): ValidationResult;
|
|
157
|
+
export {};
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Structural schema for the reviewer output contract (the final ```yaml
|
|
4
|
+
* block in `assets/agents/reviewer.md`, mirrored byte-for-byte in
|
|
5
|
+
* `assets/skill/references/contracts.md`). This module is the single
|
|
6
|
+
* hand-maintained copy of that shape in code; `test/docs-consistency.test.ts`
|
|
7
|
+
* parses the contract block itself and asserts it matches the constants
|
|
8
|
+
* below exactly, so a contract edit without a matching edit here fails the
|
|
9
|
+
* suite instead of drifting silently.
|
|
10
|
+
*
|
|
11
|
+
* This validator checks structure only. It never judges semantic adequacy,
|
|
12
|
+
* never waives a finding, and its passing is never orchestrator acceptance.
|
|
13
|
+
*/
|
|
14
|
+
/** Top-level field names, in the contract's own order. */
|
|
15
|
+
export const TOP_LEVEL_FIELDS = [
|
|
16
|
+
"status",
|
|
17
|
+
"role",
|
|
18
|
+
"task_id",
|
|
19
|
+
"summary",
|
|
20
|
+
"findings",
|
|
21
|
+
"acceptance_recommendation",
|
|
22
|
+
"missing_tests",
|
|
23
|
+
"residual_risks",
|
|
24
|
+
"reproduction",
|
|
25
|
+
"method_applied",
|
|
26
|
+
"withdrawn",
|
|
27
|
+
];
|
|
28
|
+
/** Field names of one `findings[]` entry, in the contract's own order. */
|
|
29
|
+
export const FINDING_FIELDS = [
|
|
30
|
+
"severity",
|
|
31
|
+
"category",
|
|
32
|
+
"description",
|
|
33
|
+
"suggested_fix",
|
|
34
|
+
"recurrence",
|
|
35
|
+
"introduced_by_delta",
|
|
36
|
+
];
|
|
37
|
+
/** Field names of the `reproduction` object, in the contract's own order. */
|
|
38
|
+
export const REPRODUCTION_FIELDS = [
|
|
39
|
+
"method",
|
|
40
|
+
"sample_size",
|
|
41
|
+
"result",
|
|
42
|
+
"matches_implementer_claim",
|
|
43
|
+
];
|
|
44
|
+
/** Field names of one `withdrawn[]` entry, in the contract's own order. */
|
|
45
|
+
export const WITHDRAWN_FIELDS = ["description", "reason"];
|
|
46
|
+
/**
|
|
47
|
+
* Enum spellings keyed by their bare field name. Every enum-bearing field
|
|
48
|
+
* in the contract has a name unique across the whole block, so a flat map
|
|
49
|
+
* (rather than one keyed by full path) is enough and matches how the
|
|
50
|
+
* contract text itself reads (`key: a | b | c`).
|
|
51
|
+
*/
|
|
52
|
+
export const ENUM_VALUES = {
|
|
53
|
+
status: ["reviewed"],
|
|
54
|
+
role: ["reviewer"],
|
|
55
|
+
severity: ["low", "medium", "high", "critical"],
|
|
56
|
+
category: [
|
|
57
|
+
"correctness",
|
|
58
|
+
"architecture",
|
|
59
|
+
"security",
|
|
60
|
+
"tests",
|
|
61
|
+
"maintainability",
|
|
62
|
+
"performance",
|
|
63
|
+
"docs",
|
|
64
|
+
],
|
|
65
|
+
recurrence: ["new", "repeated"],
|
|
66
|
+
introduced_by_delta: ["yes", "no", "unknown"],
|
|
67
|
+
acceptance_recommendation: [
|
|
68
|
+
"accept",
|
|
69
|
+
"accept_with_notes",
|
|
70
|
+
"fix_required",
|
|
71
|
+
"reject",
|
|
72
|
+
],
|
|
73
|
+
matches_implementer_claim: ["matched", "mismatched", "not_applicable"],
|
|
74
|
+
method_applied: ["normal", "rigorous", "adversarial"],
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* This validator reports structural validity only. It does not judge
|
|
78
|
+
* semantic adequacy, cannot waive findings, and passing it does not
|
|
79
|
+
* constitute orchestrator acceptance.
|
|
80
|
+
*/
|
|
81
|
+
export const STRUCTURAL_ONLY_NOTE = "Structural check only: this does not judge semantic adequacy, cannot waive findings, and does not constitute orchestrator acceptance.";
|
|
82
|
+
function describeValue(value) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return "missing";
|
|
85
|
+
if (value === null)
|
|
86
|
+
return "null";
|
|
87
|
+
if (Array.isArray(value))
|
|
88
|
+
return `array(length=${value.length})`;
|
|
89
|
+
if (typeof value === "object")
|
|
90
|
+
return "mapping";
|
|
91
|
+
if (typeof value === "string")
|
|
92
|
+
return JSON.stringify(value);
|
|
93
|
+
return String(value);
|
|
94
|
+
}
|
|
95
|
+
function isPlainRecord(value) {
|
|
96
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
function checkEnumField(record, key, allowed, path, diagnostics) {
|
|
99
|
+
const value = record[key];
|
|
100
|
+
const expected = allowed.join(" | ");
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
diagnostics.push({ path, expected, got: "missing" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
106
|
+
diagnostics.push({ path, expected, got: describeValue(value) });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function checkStringField(record, key, path, diagnostics) {
|
|
110
|
+
const value = record[key];
|
|
111
|
+
if (value === undefined) {
|
|
112
|
+
diagnostics.push({ path, expected: "string", got: "missing" });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (typeof value !== "string") {
|
|
116
|
+
diagnostics.push({ path, expected: "string", got: describeValue(value) });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function checkNonEmptyStringField(record, key, path, diagnostics) {
|
|
120
|
+
const value = record[key];
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
diagnostics.push({ path, expected: "non-empty string", got: "missing" });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
126
|
+
diagnostics.push({
|
|
127
|
+
path,
|
|
128
|
+
expected: "non-empty string",
|
|
129
|
+
got: describeValue(value),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Accepts a string or a number (a reviewer may write `sample_size: 5`). */
|
|
134
|
+
function checkScalarField(record, key, path, diagnostics) {
|
|
135
|
+
const value = record[key];
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
diagnostics.push({ path, expected: "string or number", got: "missing" });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
path,
|
|
143
|
+
expected: "string or number",
|
|
144
|
+
got: describeValue(value),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function checkArrayField(doc, key, diagnostics) {
|
|
149
|
+
const value = doc[key];
|
|
150
|
+
if (value === undefined) {
|
|
151
|
+
diagnostics.push({ path: key, expected: "array", got: "missing" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(value)) {
|
|
155
|
+
diagnostics.push({
|
|
156
|
+
path: key,
|
|
157
|
+
expected: "array",
|
|
158
|
+
got: describeValue(value),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Dispatch table keyed by every name in {@link FINDING_FIELDS}. The
|
|
164
|
+
* `Record<(typeof FINDING_FIELDS)[number], FieldChecker>` type means a
|
|
165
|
+
* name added to `FINDING_FIELDS` without a matching entry here is a
|
|
166
|
+
* TypeScript compile error, not a silently-unchecked field (fix-round,
|
|
167
|
+
* review finding M2).
|
|
168
|
+
*/
|
|
169
|
+
const FINDING_CHECKS = {
|
|
170
|
+
severity: (entry, path, diagnostics) => checkEnumField(entry, "severity", ENUM_VALUES.severity, `${path}.severity`, diagnostics),
|
|
171
|
+
category: (entry, path, diagnostics) => checkEnumField(entry, "category", ENUM_VALUES.category, `${path}.category`, diagnostics),
|
|
172
|
+
description: (entry, path, diagnostics) => checkStringField(entry, "description", `${path}.description`, diagnostics),
|
|
173
|
+
suggested_fix: (entry, path, diagnostics) => checkStringField(entry, "suggested_fix", `${path}.suggested_fix`, diagnostics),
|
|
174
|
+
recurrence: (entry, path, diagnostics) => checkEnumField(entry, "recurrence", ENUM_VALUES.recurrence, `${path}.recurrence`, diagnostics),
|
|
175
|
+
introduced_by_delta: (entry, path, diagnostics) => checkEnumField(entry, "introduced_by_delta", ENUM_VALUES.introduced_by_delta, `${path}.introduced_by_delta`, diagnostics),
|
|
176
|
+
};
|
|
177
|
+
/** Dispatch table keyed by every name in {@link REPRODUCTION_FIELDS}. */
|
|
178
|
+
const REPRODUCTION_CHECKS = {
|
|
179
|
+
method: (value, path, diagnostics) => checkScalarField(value, "method", `${path}.method`, diagnostics),
|
|
180
|
+
sample_size: (value, path, diagnostics) => checkScalarField(value, "sample_size", `${path}.sample_size`, diagnostics),
|
|
181
|
+
result: (value, path, diagnostics) => checkScalarField(value, "result", `${path}.result`, diagnostics),
|
|
182
|
+
matches_implementer_claim: (value, path, diagnostics) => checkEnumField(value, "matches_implementer_claim", ENUM_VALUES.matches_implementer_claim, `${path}.matches_implementer_claim`, diagnostics),
|
|
183
|
+
};
|
|
184
|
+
/** Dispatch table keyed by every name in {@link WITHDRAWN_FIELDS}. */
|
|
185
|
+
const WITHDRAWN_CHECKS = {
|
|
186
|
+
description: (entry, path, diagnostics) => checkStringField(entry, "description", `${path}.description`, diagnostics),
|
|
187
|
+
reason: (entry, path, diagnostics) => checkStringField(entry, "reason", `${path}.reason`, diagnostics),
|
|
188
|
+
};
|
|
189
|
+
function checkFindings(doc, diagnostics) {
|
|
190
|
+
const value = doc.findings;
|
|
191
|
+
if (value === undefined) {
|
|
192
|
+
diagnostics.push({ path: "findings", expected: "array", got: "missing" });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!Array.isArray(value)) {
|
|
196
|
+
diagnostics.push({
|
|
197
|
+
path: "findings",
|
|
198
|
+
expected: "array",
|
|
199
|
+
got: describeValue(value),
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
value.forEach((entry, index) => {
|
|
204
|
+
const path = `findings[${index}]`;
|
|
205
|
+
if (!isPlainRecord(entry)) {
|
|
206
|
+
diagnostics.push({
|
|
207
|
+
path,
|
|
208
|
+
expected: "mapping",
|
|
209
|
+
got: describeValue(entry),
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
for (const field of FINDING_FIELDS) {
|
|
214
|
+
FINDING_CHECKS[field](entry, path, diagnostics);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function checkReproduction(doc, diagnostics) {
|
|
219
|
+
const value = doc.reproduction;
|
|
220
|
+
if (value === undefined) {
|
|
221
|
+
diagnostics.push({
|
|
222
|
+
path: "reproduction",
|
|
223
|
+
expected: "mapping",
|
|
224
|
+
got: "missing",
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (!isPlainRecord(value)) {
|
|
229
|
+
diagnostics.push({
|
|
230
|
+
path: "reproduction",
|
|
231
|
+
expected: "mapping",
|
|
232
|
+
got: describeValue(value),
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (const field of REPRODUCTION_FIELDS) {
|
|
237
|
+
REPRODUCTION_CHECKS[field](value, "reproduction", diagnostics);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function checkWithdrawn(doc, diagnostics) {
|
|
241
|
+
const value = doc.withdrawn;
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
diagnostics.push({
|
|
244
|
+
path: "withdrawn",
|
|
245
|
+
expected: "array (may be empty)",
|
|
246
|
+
got: "missing",
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!Array.isArray(value)) {
|
|
251
|
+
diagnostics.push({
|
|
252
|
+
path: "withdrawn",
|
|
253
|
+
expected: "array (may be empty)",
|
|
254
|
+
got: describeValue(value),
|
|
255
|
+
});
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
value.forEach((entry, index) => {
|
|
259
|
+
const path = `withdrawn[${index}]`;
|
|
260
|
+
if (!isPlainRecord(entry)) {
|
|
261
|
+
diagnostics.push({
|
|
262
|
+
path,
|
|
263
|
+
expected: "mapping",
|
|
264
|
+
got: describeValue(entry),
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
for (const field of WITHDRAWN_FIELDS) {
|
|
269
|
+
WITHDRAWN_CHECKS[field](entry, path, diagnostics);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Dispatch table keyed by every name in {@link TOP_LEVEL_FIELDS}. As with
|
|
275
|
+
* {@link FINDING_CHECKS}, the `Record<(typeof TOP_LEVEL_FIELDS)[number],
|
|
276
|
+
* DocChecker>` type turns a `TOP_LEVEL_FIELDS` entry without a matching
|
|
277
|
+
* checker into a TypeScript compile error (fix-round, review finding M2):
|
|
278
|
+
* a field added to both the contract fences and this array can no longer
|
|
279
|
+
* go unchecked while every test stays green, since it fails to typecheck
|
|
280
|
+
* before any test runs.
|
|
281
|
+
*/
|
|
282
|
+
const TOP_LEVEL_CHECKS = {
|
|
283
|
+
status: (doc, diagnostics) => checkEnumField(doc, "status", ENUM_VALUES.status, "status", diagnostics),
|
|
284
|
+
role: (doc, diagnostics) => checkEnumField(doc, "role", ENUM_VALUES.role, "role", diagnostics),
|
|
285
|
+
task_id: (doc, diagnostics) => checkNonEmptyStringField(doc, "task_id", "task_id", diagnostics),
|
|
286
|
+
summary: (doc, diagnostics) => checkArrayField(doc, "summary", diagnostics),
|
|
287
|
+
findings: checkFindings,
|
|
288
|
+
acceptance_recommendation: (doc, diagnostics) => checkEnumField(doc, "acceptance_recommendation", ENUM_VALUES.acceptance_recommendation, "acceptance_recommendation", diagnostics),
|
|
289
|
+
missing_tests: (doc, diagnostics) => checkArrayField(doc, "missing_tests", diagnostics),
|
|
290
|
+
residual_risks: (doc, diagnostics) => checkArrayField(doc, "residual_risks", diagnostics),
|
|
291
|
+
reproduction: checkReproduction,
|
|
292
|
+
method_applied: (doc, diagnostics) => checkEnumField(doc, "method_applied", ENUM_VALUES.method_applied, "method_applied", diagnostics),
|
|
293
|
+
withdrawn: checkWithdrawn,
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The declared kind of every field of every contract constant, keyed by
|
|
297
|
+
* bare field name the way {@link ENUM_VALUES} is: a name is unique across
|
|
298
|
+
* the whole contract block except for `description`, which `findings[]`
|
|
299
|
+
* and `withdrawn[]` share with the same kind. The `satisfies
|
|
300
|
+
* Record<SchemaFieldName, FieldKind>` clause means a name added to any of
|
|
301
|
+
* the four constants without a kind here is a TypeScript compile error,
|
|
302
|
+
* the same way it is already an error to add one without a checker in the
|
|
303
|
+
* dispatch tables above. Were a future contract to reuse one name at two
|
|
304
|
+
* levels with two different kinds, this flat map could hold only one of
|
|
305
|
+
* them: the generator asserts each field's real diagnostics against its
|
|
306
|
+
* declared kind, so that shows up as a failing generated case rather than
|
|
307
|
+
* as an unchecked field.
|
|
308
|
+
*/
|
|
309
|
+
export const FIELD_KINDS = {
|
|
310
|
+
status: "enum",
|
|
311
|
+
role: "enum",
|
|
312
|
+
task_id: "non-empty-string",
|
|
313
|
+
summary: "array",
|
|
314
|
+
findings: "mapping-list",
|
|
315
|
+
acceptance_recommendation: "enum",
|
|
316
|
+
missing_tests: "array",
|
|
317
|
+
residual_risks: "array",
|
|
318
|
+
reproduction: "mapping",
|
|
319
|
+
method_applied: "enum",
|
|
320
|
+
withdrawn: "mapping-list",
|
|
321
|
+
severity: "enum",
|
|
322
|
+
category: "enum",
|
|
323
|
+
description: "string",
|
|
324
|
+
suggested_fix: "string",
|
|
325
|
+
recurrence: "enum",
|
|
326
|
+
introduced_by_delta: "enum",
|
|
327
|
+
method: "scalar",
|
|
328
|
+
sample_size: "scalar",
|
|
329
|
+
result: "scalar",
|
|
330
|
+
matches_implementer_claim: "enum",
|
|
331
|
+
reason: "string",
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* The `expected` text a diagnostic carries, per kind. `enum` is absent on
|
|
335
|
+
* purpose: its text is the enum's own spellings, read from
|
|
336
|
+
* {@link ENUM_VALUES}.
|
|
337
|
+
*/
|
|
338
|
+
const KIND_EXPECTED = {
|
|
339
|
+
string: "string",
|
|
340
|
+
"non-empty-string": "non-empty string",
|
|
341
|
+
scalar: "string or number",
|
|
342
|
+
array: "array",
|
|
343
|
+
"mapping-list": "array",
|
|
344
|
+
mapping: "mapping",
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Fields whose diagnostic wording differs from their kind's default.
|
|
348
|
+
* `withdrawn`'s own text spells out that an empty list is fine, since a
|
|
349
|
+
* reviewer with nothing withdrawn must still emit the key.
|
|
350
|
+
*/
|
|
351
|
+
const EXPECTED_OVERRIDES = {
|
|
352
|
+
withdrawn: "array (may be empty)",
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* The `expected` text this validator's diagnostics about `field` carry.
|
|
356
|
+
* The checkers above hold that text literally, at their own push sites;
|
|
357
|
+
* this is the schema's declaration of the same text, which the generated
|
|
358
|
+
* test cases assert the checkers actually produce, so a checker whose
|
|
359
|
+
* behaviour stops matching its declared kind fails a case instead of
|
|
360
|
+
* drifting quietly.
|
|
361
|
+
*/
|
|
362
|
+
export function expectedTextFor(field) {
|
|
363
|
+
const override = EXPECTED_OVERRIDES[field];
|
|
364
|
+
if (override !== undefined)
|
|
365
|
+
return override;
|
|
366
|
+
const kind = FIELD_KINDS[field];
|
|
367
|
+
return kind === "enum" ? ENUM_VALUES[field].join(" | ") : KIND_EXPECTED[kind];
|
|
368
|
+
}
|
|
369
|
+
/** The `expected` text a diagnostic about one element of a `mapping-list` carries. */
|
|
370
|
+
export const MAPPING_LIST_ELEMENT_EXPECTED = KIND_EXPECTED.mapping;
|
|
371
|
+
/**
|
|
372
|
+
* A reviewer return is commonly wrapped in a single fenced code block.
|
|
373
|
+
* Strips one leading/trailing fence when present, whatever language tag
|
|
374
|
+
* it carries (```yaml, ```yml, a bare ``` , or any other tag -- a
|
|
375
|
+
* language-less fence previously fell through to the "literal YAML"
|
|
376
|
+
* branch below and produced a confusing YAML parse error instead of a
|
|
377
|
+
* clean structural diagnostic; fix-round, review finding L2); anything
|
|
378
|
+
* unfenced is treated as literal YAML. The fence is located anywhere in
|
|
379
|
+
* the input, not only at its very start: a return prefixed with prose
|
|
380
|
+
* ("Here is my report:\n```yaml ...") previously fell through to the
|
|
381
|
+
* "literal YAML" branch, since the fence pattern was anchored to the
|
|
382
|
+
* start of the string, and produced a raw parse error instead of a
|
|
383
|
+
* structural diagnostic (fix-round, review finding L1). Prose found
|
|
384
|
+
* before the opening fence or after the closing fence is tolerated, but
|
|
385
|
+
* each is named as its own warning rather than silently dropped.
|
|
386
|
+
*
|
|
387
|
+
* The closing fence must start at column 0: the pattern anchors it with
|
|
388
|
+
* `^` under the `m` flag, so a triple-backtick sequence inside a value
|
|
389
|
+
* (a reviewer quoting a fenced snippet in a `description` block scalar,
|
|
390
|
+
* which YAML necessarily indents) can no longer close the block early
|
|
391
|
+
* and hand the parser a truncated document, which surfaced as
|
|
392
|
+
* diagnostics about fields the return actually carried (fix-round,
|
|
393
|
+
* review finding L3).
|
|
394
|
+
*/
|
|
395
|
+
export function extractYamlSource(raw) {
|
|
396
|
+
const warnings = [];
|
|
397
|
+
// No BOM handling: the yaml parser accepts a leading U+FEFF and the
|
|
398
|
+
// fenced path trims it away with the surrounding prose.
|
|
399
|
+
const withoutBom = raw;
|
|
400
|
+
const fenceMatch = withoutBom.match(/```[A-Za-z]*\r?\n([\s\S]*?)\r?\n?^```/m);
|
|
401
|
+
if (fenceMatch) {
|
|
402
|
+
const start = fenceMatch.index ?? 0;
|
|
403
|
+
const before = withoutBom.slice(0, start);
|
|
404
|
+
if (before.trim().length > 0) {
|
|
405
|
+
warnings.push("prose found before the opening ```yaml fence; only the fenced block was validated");
|
|
406
|
+
}
|
|
407
|
+
const inner = fenceMatch[1];
|
|
408
|
+
const after = withoutBom.slice(start + fenceMatch[0].length);
|
|
409
|
+
if (after.trim().length > 0) {
|
|
410
|
+
warnings.push("prose found after the closing ```yaml fence; only the fenced block was validated");
|
|
411
|
+
}
|
|
412
|
+
return { yamlText: inner, warnings };
|
|
413
|
+
}
|
|
414
|
+
return { yamlText: withoutBom, warnings };
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Validates a reviewer return against the reviewer output contract's
|
|
418
|
+
* structure. Reports structural validity ONLY: it never judges semantic
|
|
419
|
+
* adequacy, never waives a finding, and passing it is never orchestrator
|
|
420
|
+
* acceptance.
|
|
421
|
+
*/
|
|
422
|
+
export function validateReviewReport(raw) {
|
|
423
|
+
const diagnostics = [];
|
|
424
|
+
const { yamlText, warnings } = extractYamlSource(raw);
|
|
425
|
+
let parsed;
|
|
426
|
+
try {
|
|
427
|
+
parsed = parseYaml(yamlText);
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
diagnostics.push({
|
|
431
|
+
path: "<root>",
|
|
432
|
+
expected: "valid YAML",
|
|
433
|
+
got: error instanceof Error ? error.message : String(error),
|
|
434
|
+
});
|
|
435
|
+
return { valid: false, diagnostics, warnings };
|
|
436
|
+
}
|
|
437
|
+
if (!isPlainRecord(parsed)) {
|
|
438
|
+
diagnostics.push({
|
|
439
|
+
path: "<root>",
|
|
440
|
+
expected: "a YAML mapping (object)",
|
|
441
|
+
got: describeValue(parsed),
|
|
442
|
+
});
|
|
443
|
+
return { valid: false, diagnostics, warnings };
|
|
444
|
+
}
|
|
445
|
+
for (const field of TOP_LEVEL_FIELDS) {
|
|
446
|
+
TOP_LEVEL_CHECKS[field](parsed, diagnostics);
|
|
447
|
+
}
|
|
448
|
+
return { valid: diagnostics.length === 0, diagnostics, warnings };
|
|
449
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrator-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Installer for an orchestrator-led agent workflow: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions for Claude Code, OpenAI Codex, and opencode",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"commander": "^12.0.0",
|
|
50
|
-
"inquirer": "^9.2.0"
|
|
50
|
+
"inquirer": "^9.2.0",
|
|
51
|
+
"yaml": "^2.9.1"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@iarna/toml": "^2.2.5",
|