@fro.bot/systematic 3.13.5 → 3.13.7

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/cli.js CHANGED
@@ -1988,7 +1988,10 @@ var MAX_FINDINGS = 32;
1988
1988
  var MAX_PERSONAS = 64;
1989
1989
  var REVIEW_ARTIFACT_CUSTOM_MESSAGES = [
1990
1990
  "severity count must match rejected finding count",
1991
- "filtered findings require a validation reason"
1991
+ "filtered findings require a validation reason",
1992
+ "risk-critical dispatches require a non-empty selection surface",
1993
+ "satisfied risk coverage requires a citing input finding ID",
1994
+ "unsatisfied risk coverage must not cite an input finding ID"
1992
1995
  ];
1993
1996
  var boundedText = (maxLength) => exports_external.string().min(1).max(maxLength).regex(/\S/);
1994
1997
  var DispatchOutcomeSchema = exports_external.enum([
@@ -2012,6 +2015,14 @@ var BranchSchema = exports_external.string().max(MAX_BRANCH_LENGTH);
2012
2015
  var HeadShaSchema = exports_external.string().regex(/^[0-9a-f]{40}$/);
2013
2016
  var CompletedAtSchema = exports_external.iso.datetime({ offset: false });
2014
2017
  var ReasonSchema = boundedText(MAX_REASON_LENGTH);
2018
+ var RISK_CRITICAL_PERSONAS = [
2019
+ "security",
2020
+ "data-migrations",
2021
+ "api-contract",
2022
+ "reliability",
2023
+ "performance"
2024
+ ];
2025
+ var RiskCriticalPersonaSchema = exports_external.enum(RISK_CRITICAL_PERSONAS);
2015
2026
  var FindingTitleSchema = boundedText(256);
2016
2027
  var SeveritySchema = exports_external.enum(["P0", "P1", "P2", "P3", "unknown"]);
2017
2028
  var FindingSeveritySchema = SeveritySchema.exclude(["unknown"]);
@@ -2098,8 +2109,18 @@ var DispatchSchema = exports_external.object({
2098
2109
  persona: ReviewerSchema,
2099
2110
  dispatch_outcome: DispatchOutcomeSchema,
2100
2111
  input_finding_count: exports_external.number().int().nonnegative().max(MAX_FINDINGS),
2101
- rejection_reason: ReasonSchema.optional()
2102
- }).strict();
2112
+ rejection_reason: ReasonSchema.optional(),
2113
+ selection_surface: exports_external.array(RepoRelativePathSchema).max(MAX_FINDINGS).optional(),
2114
+ selection_reason: ReasonSchema.optional()
2115
+ }).strict().superRefine((dispatch, ctx) => {
2116
+ if (RISK_CRITICAL_PERSONAS.includes(dispatch.persona) && (!dispatch.selection_surface || dispatch.selection_surface.length === 0)) {
2117
+ ctx.addIssue({
2118
+ code: "custom",
2119
+ path: ["selection_surface"],
2120
+ message: REVIEW_ARTIFACT_CUSTOM_MESSAGES[2]
2121
+ });
2122
+ }
2123
+ });
2103
2124
  var DispositionCountsSchema = exports_external.object({
2104
2125
  surviving: exports_external.number().int().nonnegative().max(MAX_FINDINGS * MAX_PERSONAS),
2105
2126
  merged: exports_external.number().int().nonnegative().max(MAX_FINDINGS * MAX_PERSONAS),
@@ -2121,6 +2142,26 @@ var DeclinedMergeSchema = exports_external.object({
2121
2142
  input_finding_ids: exports_external.array(boundedText(MAX_INPUT_ID_LENGTH)).min(2).max(MAX_FINDINGS),
2122
2143
  reason: ReasonSchema
2123
2144
  }).strict();
2145
+ var RiskCoverageSchema = exports_external.object({
2146
+ persona: RiskCriticalPersonaSchema,
2147
+ satisfied: exports_external.boolean(),
2148
+ input_finding_id: boundedText(MAX_INPUT_ID_LENGTH).optional()
2149
+ }).strict().superRefine((coverage, ctx) => {
2150
+ if (coverage.satisfied && coverage.input_finding_id === undefined) {
2151
+ ctx.addIssue({
2152
+ code: "custom",
2153
+ path: ["input_finding_id"],
2154
+ message: REVIEW_ARTIFACT_CUSTOM_MESSAGES[3]
2155
+ });
2156
+ }
2157
+ if (!coverage.satisfied && coverage.input_finding_id !== undefined) {
2158
+ ctx.addIssue({
2159
+ code: "custom",
2160
+ path: ["input_finding_id"],
2161
+ message: REVIEW_ARTIFACT_CUSTOM_MESSAGES[4]
2162
+ });
2163
+ }
2164
+ });
2124
2165
  var ReviewArtifactSchema = exports_external.object({
2125
2166
  schema_version: exports_external.literal(1),
2126
2167
  run_id: boundedText(MAX_RUN_ID_LENGTH),
@@ -2140,6 +2181,7 @@ var ReviewArtifactSchema = exports_external.object({
2140
2181
  input_findings: exports_external.array(InputFindingSchema).max(MAX_FINDINGS * MAX_PERSONAS),
2141
2182
  findings: exports_external.array(SynthesizedFindingSchema).max(MAX_FINDINGS),
2142
2183
  declined_merges: exports_external.array(DeclinedMergeSchema).max(MAX_FINDINGS).optional(),
2184
+ risk_coverage: exports_external.array(RiskCoverageSchema).max(MAX_PERSONAS).optional(),
2143
2185
  disposition_counts: DispositionCountsSchema,
2144
2186
  applied_fixes: exports_external.array(ReasonSchema).max(MAX_FINDINGS),
2145
2187
  residual_actionable_work: exports_external.array(ReasonSchema).max(MAX_FINDINGS),
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- export declare const REVIEW_ARTIFACT_CUSTOM_MESSAGES: readonly ['severity count must match rejected finding count', 'filtered findings require a validation reason'];
2
+ export declare const REVIEW_ARTIFACT_CUSTOM_MESSAGES: readonly ['severity count must match rejected finding count', 'filtered findings require a validation reason', 'risk-critical dispatches require a non-empty selection surface', 'satisfied risk coverage requires a citing input finding ID', 'unsatisfied risk coverage must not cite an input finding ID'];
3
3
  export declare const DispatchOutcomeSchema: z.ZodEnum<{
4
4
  empty: "empty";
5
5
  findings: "findings";
@@ -134,6 +134,8 @@ export declare const ReviewArtifactSchema: z.ZodObject<{
134
134
  }>;
135
135
  input_finding_count: z.ZodNumber;
136
136
  rejection_reason: z.ZodOptional<z.ZodString>;
137
+ selection_surface: z.ZodOptional<z.ZodArray<z.ZodString>>;
138
+ selection_reason: z.ZodOptional<z.ZodString>;
137
139
  }, z.core.$strict>>;
138
140
  input_findings: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
139
141
  record_type: z.ZodLiteral<"admitted">;
@@ -223,6 +225,17 @@ export declare const ReviewArtifactSchema: z.ZodObject<{
223
225
  input_finding_ids: z.ZodArray<z.ZodString>;
224
226
  reason: z.ZodString;
225
227
  }, z.core.$strict>>>;
228
+ risk_coverage: z.ZodOptional<z.ZodArray<z.ZodObject<{
229
+ persona: z.ZodEnum<{
230
+ "api-contract": "api-contract";
231
+ "data-migrations": "data-migrations";
232
+ performance: "performance";
233
+ reliability: "reliability";
234
+ security: "security";
235
+ }>;
236
+ satisfied: z.ZodBoolean;
237
+ input_finding_id: z.ZodOptional<z.ZodString>;
238
+ }, z.core.$strict>>>;
226
239
  disposition_counts: z.ZodObject<{
227
240
  surviving: z.ZodNumber;
228
241
  merged: z.ZodNumber;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fro.bot/systematic",
3
- "version": "3.13.5",
3
+ "version": "3.13.7",
4
4
  "description": "Compound-engineering loops for OpenCode, Pi, and Claude Code",
5
5
  "type": "module",
6
6
  "homepage": "https://fro.bot/systematic",
@@ -52,7 +52,7 @@ tags: [keyword-one, keyword-two]
52
52
 
53
53
  ## Knowledge Track Template
54
54
 
55
- Use for: `best_practice`, `documentation_gap`, `workflow_issue`, `developer_experience`
55
+ Use for: `best_practice`, `documentation_gap`, `workflow_issue`, `developer_experience`, `architecture_pattern`, `design_pattern`, `tooling_decision`, `convention`
56
56
 
57
57
  ```markdown
58
58
  ---
@@ -52,7 +52,7 @@ tags: [keyword-one, keyword-two]
52
52
 
53
53
  ## Knowledge Track Template
54
54
 
55
- Use for: `best_practice`, `documentation_gap`, `workflow_issue`, `developer_experience`
55
+ Use for: `best_practice`, `documentation_gap`, `workflow_issue`, `developer_experience`, `architecture_pattern`, `design_pattern`, `tooling_decision`, `convention`
56
56
 
57
57
  ```markdown
58
58
  ---
@@ -360,6 +360,10 @@ Review team:
360
360
 
361
361
  This is progress reporting, not a blocking confirmation.
362
362
 
363
+ Record each conditional persona's selection reason and triggering file paths
364
+ on its dispatch record; see the [synthesis artifact contract](./references/synthesis-artifact-contract.md)
365
+ for the field semantics.
366
+
363
367
  ### Stage 3b: Discover project standards paths
364
368
 
365
369
  Before spawning sub-agents, find the file paths (not contents) of all relevant standards files for the `project-standards` persona. Use the native file-search/glob tool to locate:
@@ -520,8 +524,8 @@ Assemble the final report using **pipe-delimited markdown tables for findings**
520
524
  8. **Learnings & Past Solutions.** Surface learnings-researcher results: if past solutions are relevant, flag them as "Known Pattern" with links to docs/solutions/ files.
521
525
  9. **Agent-Native Gaps.** Surface agent-native-reviewer results. Omit section if no gaps found.
522
526
  10. **Deployment Notes.** If deployment-verification-agent ran, surface the key Go/No-Go items: blocking pre-deploy checks, the most important verification queries, rollback caveats, and monitoring focus areas. Keep the checklist actionable rather than dropping it into Coverage.
523
- 11. **Coverage.** Suppressed count, residual risks, testing gaps, failed/timed-out reviewers, validator failures, and any intent uncertainty carried by non-interactive modes.
524
- 12. **Verdict.** Ready to merge / Ready with fixes / Not ready. Fix order if applicable. When an `explicit` plan has unaddressed requirements, the verdict must reflect it — a PR that's code-clean but missing planned requirements is "Not ready" unless the omission is intentional. When an `inferred` plan has unaddressed requirements, note it in the verdict reasoning but do not block on it alone. Apply the risk-aware degraded verdict rule from the [synthesis artifact contract](./references/synthesis-artifact-contract.md).
527
+ 11. **Coverage.** Suppressed count, residual risks, testing gaps, failed/timed-out reviewers, validator failures, risk-coverage entries with citing input finding IDs and exit conditions for blocked entries, and any intent uncertainty carried by non-interactive modes.
528
+ 12. **Verdict.** Ready to merge / Ready with fixes / Not ready. Fix order if applicable. When an `explicit` plan has unaddressed requirements, the verdict must reflect it — a PR that's code-clean but missing planned requirements is "Not ready" unless the omission is intentional. When an `inferred` plan has unaddressed requirements, note it in the verdict reasoning but do not block on it alone. Apply the risk-aware degraded verdict rule from the [synthesis artifact contract](./references/synthesis-artifact-contract.md), including the recorded exit condition for a blocked risk-critical verdict.
525
529
 
526
530
  Do not include time estimates.
527
531
 
@@ -137,7 +137,7 @@ This fails because: no pipe-delimited tables, no severity-grouped `###` headers,
137
137
  - **Learnings & Past Solutions section** -- results from learnings-researcher, with links to docs/solutions/ files
138
138
  - **Agent-Native Gaps section** -- results from agent-native-reviewer. Omit if no gaps found.
139
139
  - **Deployment Notes section** -- key checklist items from deployment-verification-agent. Omit if the agent did not run.
140
- - **Coverage section** -- suppressed count with original confidences, residual risks, testing gaps, failed reviewers, and disposition reconciliation
140
+ - **Coverage section** -- suppressed count with original confidences, residual risks, testing gaps, failed reviewers, disposition reconciliation, and risk-coverage entries with their citing input finding IDs and blocked-entry exit conditions
141
141
  - **Summary uses blockquotes** for verdict, reasoning, and fix order
142
142
  - **Horizontal rule** (`---`) separates findings from verdict
143
143
  - **`###` headers** for each section -- never plain text headers
@@ -69,6 +69,31 @@
69
69
  "minLength": 1,
70
70
  "maxLength": 2048,
71
71
  "pattern": "\\S"
72
+ },
73
+ "selection_surface": {
74
+ "maxItems": 32,
75
+ "type": "array",
76
+ "items": {
77
+ "type": "string",
78
+ "minLength": 1,
79
+ "maxLength": 256,
80
+ "allOf": [
81
+ {
82
+ "type": "string",
83
+ "pattern": "\\S"
84
+ },
85
+ {
86
+ "type": "string",
87
+ "pattern": "^(?!\\/)(?![A-Za-z]:[\\\\/])(?!\\\\).+"
88
+ }
89
+ ]
90
+ }
91
+ },
92
+ "selection_reason": {
93
+ "type": "string",
94
+ "minLength": 1,
95
+ "maxLength": 2048,
96
+ "pattern": "\\S"
72
97
  }
73
98
  },
74
99
  "required": ["persona", "dispatch_outcome", "input_finding_count"],
@@ -417,6 +442,36 @@
417
442
  "additionalProperties": false
418
443
  }
419
444
  },
445
+ "risk_coverage": {
446
+ "maxItems": 64,
447
+ "type": "array",
448
+ "items": {
449
+ "type": "object",
450
+ "properties": {
451
+ "persona": {
452
+ "type": "string",
453
+ "enum": [
454
+ "security",
455
+ "data-migrations",
456
+ "api-contract",
457
+ "reliability",
458
+ "performance"
459
+ ]
460
+ },
461
+ "satisfied": {
462
+ "type": "boolean"
463
+ },
464
+ "input_finding_id": {
465
+ "type": "string",
466
+ "minLength": 1,
467
+ "maxLength": 128,
468
+ "pattern": "\\S"
469
+ }
470
+ },
471
+ "required": ["persona", "satisfied"],
472
+ "additionalProperties": false
473
+ }
474
+ },
420
475
  "disposition_counts": {
421
476
  "type": "object",
422
477
  "properties": {
@@ -105,7 +105,10 @@ The artifact must preserve these distinctions:
105
105
  records what a persona returned: `findings`, `empty`, `malformed`, or
106
106
  `never_returned`. A rejection reason is the exact safe validation reason,
107
107
  naming persona and field without echoing the offending value. Dispatch
108
- outcome is separate from finding disposition.
108
+ outcome is separate from finding disposition. Conditional selections record
109
+ their triggering changed-file paths in `selection_surface` and the announced
110
+ selection explanation in `selection_reason`; always-on personas may omit
111
+ both fields.
109
112
  - `input_findings` is the authoritative parent-owned ledger. Before the
110
113
  confidence gate, every admitted finding receives an `input_id` of
111
114
  `<reviewer>#<1-based finding index>`. Every admitted input has exactly one
@@ -207,10 +210,17 @@ validating it. This ordering makes the artifact validatable at all: without
207
210
  `schema_version`, the validator reports the legacy status (exit 3) rather than
208
211
  a real validation result.
209
212
 
210
- After writing `review-summary.json`, the parent runs
211
- `systematic validate-review-artifact <path>` against it. A nonzero exit means
212
- the run is not complete. The [executable schema](./review-summary-schema.json)
213
- is generated from a Zod source and is the machine-checkable form of the shape
213
+ After writing `review-summary.json`, the parent checks whether the
214
+ `systematic` executable is available on the invoking environment's `PATH`.
215
+ When it is available, the parent runs
216
+ `systematic validate-review-artifact <path>` against it. The executable ships
217
+ through the npm package's `bin` entry; a harness that installs bundled
218
+ markdown without that package will not have it. When it is unavailable, the
219
+ parent records that validation did not run and why in the run record.
220
+ Unavailable validation is distinct from skipped validation, and the parent
221
+ does not represent the artifact as validated. A nonzero exit means the run is
222
+ not complete. The [executable schema](./review-summary-schema.json) is
223
+ generated from a Zod source and is the machine-checkable form of the shape
214
224
  described here.
215
225
 
216
226
  On validation failure, the parent repairs the artifact and re-runs the
@@ -223,7 +233,8 @@ This is enforcement by visible failure, not by containment. An agent that
223
233
  never runs the command can still finalize an artifact, but produces no evidence
224
234
  in either direction. That is why the command exists as an independently
225
235
  runnable check rather than as a self-validation instruction, and why its result
226
- belongs in the run record.
236
+ belongs in the run record. An unavailable validator is recorded as unavailable,
237
+ not as skipped or validated.
227
238
 
228
239
  `mode:report-only` writes no artifact and therefore performs no validation.
229
240
 
@@ -246,18 +257,20 @@ The risk-critical surfaces are `security`, `data-migrations`, `api-contract`,
246
257
  specifically for the matching diff shape in Stage 3. If one of those selected
247
258
  personas has `dispatch_outcome: "malformed"` or
248
259
  `dispatch_outcome: "never_returned"`, the review verdict must not be clean:
249
- it is blocking unless another persona covered the same surface and returned
250
- validated evidence for it. For this rule, validated evidence means at least
260
+ it is blocking unless another persona covered the lost surface with validated
261
+ evidence. For this rule, a validated finding from another persona covers a
262
+ lost risk-critical surface if and only if the finding's `file` appears in the
263
+ lost persona's recorded `selection_surface`; validated evidence means at least
251
264
  one finding from that other persona's return passed complete schema and
252
- environment-value validation and is relevant to the same surface. A coverage
253
- note alone cannot satisfy this rule; the verdict must reflect the missing
254
- risk-critical evidence.
265
+ environment-value validation. A coverage note alone cannot satisfy this rule;
266
+ the verdict must reflect the missing risk-critical evidence.
255
267
 
256
268
  Finding-level rejection is keyed by the severities in
257
269
  `rejected_severities`. A selected risk-critical persona whose rejected
258
270
  findings include any `P0`, `P1`, or `unknown` severity is treated exactly as a
259
271
  rejected persona for this verdict rule: blocking unless another persona
260
- covered the same surface with validated evidence. A selected risk-critical
272
+ covered the lost surface with a validated finding whose `file` appears in the
273
+ lost persona's recorded `selection_surface`. A selected risk-critical
261
274
  persona whose rejected findings are only `P2` or `P3` does not block on that
262
275
  basis alone; record it in the Coverage section instead. Unknown severity is
263
276
  treated as blocking as deliberate fail-closed behavior because the parent
@@ -265,3 +278,12 @@ could not determine what was lost. Admitted findings and verdict blocking
265
278
  are independent: surviving findings from the same return continue through
266
279
  synthesis normally. Partial return is not partial coverage when the lost
267
280
  part was critical.
281
+
282
+ Record each lost risk-critical persona in the optional `risk_coverage` array
283
+ with `satisfied: true` and `input_finding_id` set to the citing input finding
284
+ ID when coverage is satisfied, or `satisfied: false` without an
285
+ `input_finding_id` otherwise. A blocked verdict must name its exit condition
286
+ in the report: re-run the lost persona, or supply a validated finding from
287
+ another persona whose `file` appears in the lost persona's recorded
288
+ `selection_surface`. Derive that condition from the dispatch and risk-coverage
289
+ records, not from a coverage note.