@momentiq/dark-factory-cli 1.0.0 → 2.0.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 +121 -40
- package/dist/adapters/codex-sdk.d.ts +66 -0
- package/dist/adapters/codex-sdk.d.ts.map +1 -1
- package/dist/adapters/codex-sdk.js +321 -4
- package/dist/adapters/codex-sdk.js.map +1 -1
- package/dist/adapters/critic-result-schema.d.ts +15 -10
- package/dist/adapters/critic-result-schema.d.ts.map +1 -1
- package/dist/adapters/critic-result-schema.js +10 -0
- package/dist/adapters/critic-result-schema.js.map +1 -1
- package/dist/adapters/cursor-cli.d.ts +19 -1
- package/dist/adapters/cursor-cli.d.ts.map +1 -1
- package/dist/adapters/cursor-cli.js +96 -14
- package/dist/adapters/cursor-cli.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/static-schema-lint.d.ts +68 -0
- package/dist/adapters/static-schema-lint.d.ts.map +1 -0
- package/dist/adapters/static-schema-lint.js +813 -0
- package/dist/adapters/static-schema-lint.js.map +1 -0
- package/dist/branch-protection/spec-default.yaml +2 -2
- package/dist/cli.d.ts +17 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +414 -113
- package/dist/cli.js.map +1 -1
- package/dist/commands/findings.d.ts +6 -0
- package/dist/commands/findings.d.ts.map +1 -0
- package/dist/commands/findings.js +196 -0
- package/dist/commands/findings.js.map +1 -0
- package/dist/commands/show.d.ts +11 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +78 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/skills.d.ts +6 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +248 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +85 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/compact/index.d.ts +2 -0
- package/dist/compact/index.d.ts.map +1 -0
- package/dist/compact/index.js +3 -0
- package/dist/compact/index.js.map +1 -0
- package/dist/compact/lockfile.d.ts +53 -0
- package/dist/compact/lockfile.d.ts.map +1 -0
- package/dist/compact/lockfile.js +626 -0
- package/dist/compact/lockfile.js.map +1 -0
- package/dist/cycle-doc-validator/validate_cycle_doc.py +39 -9
- package/dist/doctor.d.ts +51 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +497 -1
- package/dist/doctor.js.map +1 -1
- package/dist/evidence/docker-build.d.ts +6 -0
- package/dist/evidence/docker-build.d.ts.map +1 -0
- package/dist/evidence/docker-build.js +199 -0
- package/dist/evidence/docker-build.js.map +1 -0
- package/dist/evidence/index.d.ts +1 -0
- package/dist/evidence/index.d.ts.map +1 -1
- package/dist/evidence/index.js +6 -0
- package/dist/evidence/index.js.map +1 -1
- package/dist/handoff/handoff-verb.d.ts.map +1 -1
- package/dist/handoff/handoff-verb.js +6 -3
- package/dist/handoff/handoff-verb.js.map +1 -1
- package/dist/handoff/ports.d.ts +8 -0
- package/dist/handoff/ports.d.ts.map +1 -1
- package/dist/handoff/real-clients.js +1 -1
- package/dist/handoff/real-clients.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/show-status-core.d.ts +63 -0
- package/dist/lib/show-status-core.d.ts.map +1 -0
- package/dist/lib/show-status-core.js +156 -0
- package/dist/lib/show-status-core.js.map +1 -0
- package/dist/mcp/prompts.d.ts.map +1 -1
- package/dist/mcp/prompts.js +113 -78
- package/dist/mcp/prompts.js.map +1 -1
- package/dist/mcp/resources.js +1 -1
- package/dist/mcp/resources.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/doctor.d.ts.map +1 -1
- package/dist/mcp/tools/doctor.js +5 -0
- package/dist/mcp/tools/doctor.js.map +1 -1
- package/dist/mcp/tools/findings.d.ts +0 -22
- package/dist/mcp/tools/findings.d.ts.map +1 -1
- package/dist/mcp/tools/findings.js +5 -73
- package/dist/mcp/tools/findings.js.map +1 -1
- package/dist/mcp/tools/handoff.d.ts.map +1 -1
- package/dist/mcp/tools/handoff.js +10 -17
- package/dist/mcp/tools/handoff.js.map +1 -1
- package/dist/mcp/tools/review-bypass.d.ts.map +1 -1
- package/dist/mcp/tools/review-bypass.js +22 -1
- package/dist/mcp/tools/review-bypass.js.map +1 -1
- package/dist/mcp/tools/skills-install.d.ts +6 -0
- package/dist/mcp/tools/skills-install.d.ts.map +1 -0
- package/dist/mcp/tools/skills-install.js +260 -0
- package/dist/mcp/tools/skills-install.js.map +1 -0
- package/dist/mcp/tools/stats-gate.d.ts.map +1 -1
- package/dist/mcp/tools/stats-gate.js +63 -15
- package/dist/mcp/tools/stats-gate.js.map +1 -1
- package/dist/policy/baseline.d.ts.map +1 -1
- package/dist/policy/baseline.js +11 -3
- package/dist/policy/baseline.js.map +1 -1
- package/dist/policy/gate.d.ts +1 -1
- package/dist/policy/gate.d.ts.map +1 -1
- package/dist/policy/gate.js +96 -12
- package/dist/policy/gate.js.map +1 -1
- package/dist/prompt.d.ts +1 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +123 -4
- package/dist/prompt.js.map +1 -1
- package/dist/report.d.ts +130 -3
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +367 -10
- package/dist/report.js.map +1 -1
- package/dist/runner.d.ts +6 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +213 -4
- package/dist/runner.js.map +1 -1
- package/dist/self-consistency.d.ts +144 -0
- package/dist/self-consistency.d.ts.map +1 -0
- package/dist/self-consistency.js +368 -0
- package/dist/self-consistency.js.map +1 -0
- package/dist/skills/config.d.ts +176 -0
- package/dist/skills/config.d.ts.map +1 -0
- package/dist/skills/config.js +251 -0
- package/dist/skills/config.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +8 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/install.d.ts +62 -0
- package/dist/skills/install.d.ts.map +1 -0
- package/dist/skills/install.js +315 -0
- package/dist/skills/install.js.map +1 -0
- package/dist/skills/template.d.ts +42 -0
- package/dist/skills/template.d.ts.map +1 -0
- package/dist/skills/template.js +95 -0
- package/dist/skills/template.js.map +1 -0
- package/dist/trusted-surface/rebind.d.ts.map +1 -1
- package/dist/trusted-surface/rebind.js +199 -2
- package/dist/trusted-surface/rebind.js.map +1 -1
- package/package.json +36 -2
- package/skills/README.md +89 -0
- package/skills/chief-engineer-blitz/SKILL.md.tmpl +443 -0
- package/skills/chief-engineer-blitz/skill.json +53 -0
- package/skills/chief-engineer-review/SKILL.md.tmpl +184 -0
- package/skills/chief-engineer-review/references/review-examples.md.tmpl +130 -0
- package/skills/chief-engineer-review/skill.json +67 -0
- package/skills/chief-engineer-review/templates/code-review-prompt.md.tmpl +111 -0
- package/skills/chief-engineer-review/templates/escalation-prompt.md.tmpl +56 -0
- package/skills/chief-engineer-review/templates/plan-review-prompt.md.tmpl +74 -0
- package/skills/skill-schema.json +73 -0
package/dist/report.d.ts
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
import type { LoadedConfig } from "./policy/config.js";
|
|
2
|
-
import { type CriticResult, type ReviewArtifact, type ReviewSeverity, type ReviewVerdict } from "@momentiq/dark-factory-schemas";
|
|
2
|
+
import { type CriticResult, type ReviewArtifact, type ReviewFinding, type ReviewSeverity, type ReviewVerdict, type UnilateralVetoRules } from "@momentiq/dark-factory-schemas";
|
|
3
|
+
/**
|
|
4
|
+
* True when the finding carries any of the flags listed in
|
|
5
|
+
* `requireCorroborationFor`. Flags not present in `FLAG_TO_FINDING_KEY`
|
|
6
|
+
* are ignored — an unknown flag in config is a no-op rather than a
|
|
7
|
+
* runtime error so operators can pin policy without first deploying a
|
|
8
|
+
* CLI that knows the flag (forward-compat).
|
|
9
|
+
*
|
|
10
|
+
* Exported so `gate.ts` shares the canonical translation table and
|
|
11
|
+
* cannot drift.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findingCarriesCorroborationFlag(finding: ReviewFinding, flagNames: readonly string[]): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* True when at least one OTHER completed critic has a blocking-severity
|
|
16
|
+
* finding on the SAME file within `radius` lines of `target`.
|
|
17
|
+
*
|
|
18
|
+
* Exported so `gate.ts` shares the canonical corroboration predicate
|
|
19
|
+
* and cannot drift.
|
|
20
|
+
*
|
|
21
|
+
* "Other" = different `criticId`. Critics never corroborate themselves.
|
|
22
|
+
* "Blocking-severity" = `blockingSeverities.includes(f.severity)`. The
|
|
23
|
+
* corroborator is checked WITHOUT recursing into the corroboration
|
|
24
|
+
* rule — a corroborator that itself carries `selfInconsistent: true`
|
|
25
|
+
* still corroborates (the spec's safety net is "another critic saw a
|
|
26
|
+
* blocker here too", not "another critic's blocker survived
|
|
27
|
+
* corroboration"). This intentional asymmetry prevents an N-way
|
|
28
|
+
* corroboration-cycle.
|
|
29
|
+
*
|
|
30
|
+
* `target.file === undefined` → returns false (corroboration is
|
|
31
|
+
* file-scoped; a finding without a file cannot be corroborated).
|
|
32
|
+
* `target.line === undefined` → treats the target line as `0` (matches
|
|
33
|
+
* any line in the same file within the radius window — the safety
|
|
34
|
+
* net errs toward "yes, this is corroborated" when the target's
|
|
35
|
+
* location is imprecise).
|
|
36
|
+
*/
|
|
37
|
+
export declare function isCorroboratedByOtherCritic(target: ReviewFinding, targetCriticId: string, allResults: readonly CriticResult[], blockingSeverities: readonly ReviewSeverity[], radius: number): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* A finding the aggregator demoted from a unilateral veto to an
|
|
40
|
+
* informational `critic_disagreement` note because the
|
|
41
|
+
* `unilateralVetoRules.requireCorroborationFor` policy fired AND no
|
|
42
|
+
* other critic corroborated within the radius. Surfaced on
|
|
43
|
+
* `QuorumAggregateOutcome` so the runner can emit
|
|
44
|
+
* `critic_disagreement` telemetry and the artifact writer can render
|
|
45
|
+
* the note in the per-run markdown.
|
|
46
|
+
*/
|
|
47
|
+
export interface CriticDisagreementNote {
|
|
48
|
+
criticId: string;
|
|
49
|
+
file: string;
|
|
50
|
+
line?: number;
|
|
51
|
+
severity: ReviewSeverity;
|
|
52
|
+
/**
|
|
53
|
+
* The corroboration-flag name from the policy that triggered the
|
|
54
|
+
* demotion (currently always `"self_inconsistent"`). Future flags
|
|
55
|
+
* surface here without a new field.
|
|
56
|
+
*/
|
|
57
|
+
flag: string;
|
|
58
|
+
/** Short summary cut from the finding's `evidence` for the note. */
|
|
59
|
+
evidence: string;
|
|
60
|
+
}
|
|
3
61
|
export interface AggregateInputs {
|
|
4
62
|
loaded: LoadedConfig;
|
|
5
63
|
commit: string;
|
|
@@ -19,6 +77,26 @@ export declare function buildAggregate(inputs: AggregateInputs): ReviewArtifact;
|
|
|
19
77
|
* computation see the same definition of "completed".
|
|
20
78
|
*/
|
|
21
79
|
export declare function isCriticCompleted(r: CriticResult): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Optional context for the corroboration check
|
|
82
|
+
* (issue dark-factory-platform#112). When omitted, `criticVetoesGate`
|
|
83
|
+
* behaves exactly as the pre-#112 implementation (back-compat for the
|
|
84
|
+
* many callers that don't know about the policy).
|
|
85
|
+
*/
|
|
86
|
+
export interface VetoCorroborationContext {
|
|
87
|
+
/** All critic results in this review run — used to look for
|
|
88
|
+
* cross-critic corroboration on the same file within radius. */
|
|
89
|
+
allResults: readonly CriticResult[];
|
|
90
|
+
rules: UnilateralVetoRules;
|
|
91
|
+
/**
|
|
92
|
+
* Optional sink for findings the aggregator demoted to a
|
|
93
|
+
* `critic_disagreement` note. The caller pre-allocates the array;
|
|
94
|
+
* this function pushes one entry per demotion. Surfaced as
|
|
95
|
+
* `QuorumAggregateOutcome.disagreements` so the runner can emit
|
|
96
|
+
* telemetry without re-walking the results.
|
|
97
|
+
*/
|
|
98
|
+
disagreementSink?: CriticDisagreementNote[];
|
|
99
|
+
}
|
|
22
100
|
/**
|
|
23
101
|
* Cycle 322.3 — does this completed critic veto the gate?
|
|
24
102
|
*
|
|
@@ -27,8 +105,24 @@ export declare function isCriticCompleted(r: CriticResult): boolean;
|
|
|
27
105
|
* CHANGES_REQUESTED verdict) blocks the gate regardless of how many
|
|
28
106
|
* other critics approved. Returns `false` for non-completed critics
|
|
29
107
|
* — only a completed critic can veto.
|
|
108
|
+
*
|
|
109
|
+
* Issue dark-factory-platform#112 — when `corroborationCtx` is
|
|
110
|
+
* supplied AND the critic's only veto trigger is a blocking finding
|
|
111
|
+
* carrying one of the configured corroboration-required flags (e.g.
|
|
112
|
+
* `selfInconsistent: true`), the function checks whether ANOTHER
|
|
113
|
+
* critic raises a blocking finding on the same file within
|
|
114
|
+
* `rules.requireCorroborationOnHunkRadius` lines. If not, the
|
|
115
|
+
* finding is demoted to a `critic_disagreement` note (pushed to
|
|
116
|
+
* `disagreementSink`) and DOES NOT contribute to a veto. Findings
|
|
117
|
+
* without a flag still veto unconditionally — the safety net is
|
|
118
|
+
* intact for findings the critic can defend.
|
|
119
|
+
*
|
|
120
|
+
* Note: a critic that vetoes via `CHANGES_REQUESTED` verdict OR
|
|
121
|
+
* `requiresHumanJudgment` is NOT subject to corroboration — those
|
|
122
|
+
* are coarser-grained signals than per-finding flags and the spec
|
|
123
|
+
* narrows the rule to per-finding flag triggers only.
|
|
30
124
|
*/
|
|
31
|
-
export declare function criticVetoesGate(r: CriticResult, blockingSeverities: ReviewSeverity[]): boolean;
|
|
125
|
+
export declare function criticVetoesGate(r: CriticResult, blockingSeverities: ReviewSeverity[], corroborationCtx?: VetoCorroborationContext): boolean;
|
|
32
126
|
export interface QuorumAggregateOutcome {
|
|
33
127
|
verdict: ReviewVerdict;
|
|
34
128
|
/**
|
|
@@ -48,6 +142,15 @@ export interface QuorumAggregateOutcome {
|
|
|
48
142
|
reason: "majority" | "veto" | "quorum_unmet";
|
|
49
143
|
completedCount: number;
|
|
50
144
|
totalCount: number;
|
|
145
|
+
/**
|
|
146
|
+
* Issue dark-factory-platform#112 — findings the aggregator demoted
|
|
147
|
+
* from a unilateral veto to an informational disagreement note
|
|
148
|
+
* because the `unilateralVetoRules.requireCorroborationFor` policy
|
|
149
|
+
* fired AND no other critic corroborated. Always present (empty
|
|
150
|
+
* array when no demotions occurred OR no policy was active) so
|
|
151
|
+
* callers can branch on `.length` without an undefined check.
|
|
152
|
+
*/
|
|
153
|
+
disagreements: CriticDisagreementNote[];
|
|
51
154
|
}
|
|
52
155
|
/**
|
|
53
156
|
* Cycle 322.3 — compute the aggregate verdict under
|
|
@@ -70,7 +173,7 @@ export interface QuorumAggregateOutcome {
|
|
|
70
173
|
* property: an outage that knocks out two critics doesn't paralyze
|
|
71
174
|
* the third critic's BLOCKER finding.
|
|
72
175
|
*/
|
|
73
|
-
export declare function quorumAggregateVerdict(results: CriticResult[], blockingSeverities: ReviewSeverity[], quorum: number): QuorumAggregateOutcome;
|
|
176
|
+
export declare function quorumAggregateVerdict(results: CriticResult[], blockingSeverities: ReviewSeverity[], quorum: number, unilateralVetoRules?: UnilateralVetoRules): QuorumAggregateOutcome;
|
|
74
177
|
export interface CriticReport {
|
|
75
178
|
/** Full multi-line block for stdout (CI log + local terminal). */
|
|
76
179
|
stdout: string;
|
|
@@ -98,6 +201,30 @@ export interface CriticReport {
|
|
|
98
201
|
* "X/Y critics errored" line agrees with the quorum aggregator's view.
|
|
99
202
|
*/
|
|
100
203
|
export declare function buildCriticReport(artifact: ReviewArtifact, jsonPath: string): CriticReport;
|
|
204
|
+
export interface ZeroEvidenceDiagnostic {
|
|
205
|
+
/**
|
|
206
|
+
* True when completedCount === 0 AND totalCount > 0. The caller writes
|
|
207
|
+
* `stderr` to process.stderr when this is true and skips otherwise.
|
|
208
|
+
*/
|
|
209
|
+
isZeroEvidence: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* The formatted stderr block, including the leading "df: review
|
|
212
|
+
* COMPLETED..." headline, per-critic remediation lines, and the
|
|
213
|
+
* "details:" footer. Empty string when `isZeroEvidence` is false.
|
|
214
|
+
*/
|
|
215
|
+
stderr: string;
|
|
216
|
+
}
|
|
217
|
+
export interface ZeroEvidenceOptions {
|
|
218
|
+
/**
|
|
219
|
+
* Hint from the caller that the loaded config has no `profiles` block
|
|
220
|
+
* (or the requested profile name is missing from it). When false, the
|
|
221
|
+
* diagnostic surfaces a top-line remediation pointing the operator at
|
|
222
|
+
* `.agent-review/config.json`. When undefined, no top-line config hint
|
|
223
|
+
* is added — only per-critic hints are emitted.
|
|
224
|
+
*/
|
|
225
|
+
configHasProfiles?: boolean;
|
|
226
|
+
}
|
|
227
|
+
export declare function buildZeroEvidenceDiagnostic(artifact: ReviewArtifact, jsonPath: string, options?: ZeroEvidenceOptions): ZeroEvidenceDiagnostic;
|
|
101
228
|
export interface WriteResult {
|
|
102
229
|
jsonPath: string;
|
|
103
230
|
markdownPath: string | null;
|
package/dist/report.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAOvD,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
|
package/dist/report.js
CHANGED
|
@@ -1,6 +1,87 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { artifactJsonPath, artifactLockPath, artifactMarkdownPath, resolveArtifactDir, } from "./paths.js";
|
|
3
3
|
import { parseReviewArtifact, REVIEW_SEVERITIES, } from "@momentiq/dark-factory-schemas";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Issue dark-factory-platform#112 — flag-name → ReviewFinding-key mapping.
|
|
6
|
+
//
|
|
7
|
+
// The policy config (`AggregationConfig.unilateralVetoRules.requireCorroborationFor`)
|
|
8
|
+
// uses snake_case opaque identifiers so the wire surface is stable
|
|
9
|
+
// independent of the TypeScript field name. The aggregator translates
|
|
10
|
+
// each identifier to the corresponding `ReviewFinding` boolean property
|
|
11
|
+
// at evaluation time. Future flags (e.g. `requires_human_judgment`,
|
|
12
|
+
// `low_confidence`) land here when they need the same
|
|
13
|
+
// corroboration-or-disagreement-note treatment.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const FLAG_TO_FINDING_KEY = {
|
|
16
|
+
self_inconsistent: "selfInconsistent",
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* True when the finding carries any of the flags listed in
|
|
20
|
+
* `requireCorroborationFor`. Flags not present in `FLAG_TO_FINDING_KEY`
|
|
21
|
+
* are ignored — an unknown flag in config is a no-op rather than a
|
|
22
|
+
* runtime error so operators can pin policy without first deploying a
|
|
23
|
+
* CLI that knows the flag (forward-compat).
|
|
24
|
+
*
|
|
25
|
+
* Exported so `gate.ts` shares the canonical translation table and
|
|
26
|
+
* cannot drift.
|
|
27
|
+
*/
|
|
28
|
+
export function findingCarriesCorroborationFlag(finding, flagNames) {
|
|
29
|
+
for (const name of flagNames) {
|
|
30
|
+
const key = FLAG_TO_FINDING_KEY[name];
|
|
31
|
+
if (key === undefined)
|
|
32
|
+
continue;
|
|
33
|
+
const v = finding[key];
|
|
34
|
+
if (v === true)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when at least one OTHER completed critic has a blocking-severity
|
|
41
|
+
* finding on the SAME file within `radius` lines of `target`.
|
|
42
|
+
*
|
|
43
|
+
* Exported so `gate.ts` shares the canonical corroboration predicate
|
|
44
|
+
* and cannot drift.
|
|
45
|
+
*
|
|
46
|
+
* "Other" = different `criticId`. Critics never corroborate themselves.
|
|
47
|
+
* "Blocking-severity" = `blockingSeverities.includes(f.severity)`. The
|
|
48
|
+
* corroborator is checked WITHOUT recursing into the corroboration
|
|
49
|
+
* rule — a corroborator that itself carries `selfInconsistent: true`
|
|
50
|
+
* still corroborates (the spec's safety net is "another critic saw a
|
|
51
|
+
* blocker here too", not "another critic's blocker survived
|
|
52
|
+
* corroboration"). This intentional asymmetry prevents an N-way
|
|
53
|
+
* corroboration-cycle.
|
|
54
|
+
*
|
|
55
|
+
* `target.file === undefined` → returns false (corroboration is
|
|
56
|
+
* file-scoped; a finding without a file cannot be corroborated).
|
|
57
|
+
* `target.line === undefined` → treats the target line as `0` (matches
|
|
58
|
+
* any line in the same file within the radius window — the safety
|
|
59
|
+
* net errs toward "yes, this is corroborated" when the target's
|
|
60
|
+
* location is imprecise).
|
|
61
|
+
*/
|
|
62
|
+
export function isCorroboratedByOtherCritic(target, targetCriticId, allResults, blockingSeverities, radius) {
|
|
63
|
+
if (!target.file)
|
|
64
|
+
return false;
|
|
65
|
+
const targetLine = target.line ?? 0;
|
|
66
|
+
for (const other of allResults) {
|
|
67
|
+
if (other.criticId === targetCriticId)
|
|
68
|
+
continue;
|
|
69
|
+
if (other.status !== "complete")
|
|
70
|
+
continue;
|
|
71
|
+
for (const f of other.findings) {
|
|
72
|
+
if (!blockingSeverities.includes(f.severity))
|
|
73
|
+
continue;
|
|
74
|
+
if (f.file !== target.file)
|
|
75
|
+
continue;
|
|
76
|
+
if (target.line === undefined || f.line === undefined)
|
|
77
|
+
return true;
|
|
78
|
+
const distance = Math.abs(f.line - targetLine);
|
|
79
|
+
if (distance <= radius)
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
4
85
|
const SEVERITY_RANK = {
|
|
5
86
|
blocker: 0,
|
|
6
87
|
high: 1,
|
|
@@ -14,9 +95,18 @@ export function buildAggregate(inputs) {
|
|
|
14
95
|
const aggregationPolicy = loaded.config.aggregation.policy;
|
|
15
96
|
const blockingSeverities = loaded.config.aggregation.blockingSeverities;
|
|
16
97
|
let gateVerdict;
|
|
98
|
+
let disagreements = [];
|
|
17
99
|
if (status === "complete") {
|
|
18
|
-
|
|
100
|
+
const aggregate = aggregateOutcome(criticResults, blockingSeverities, loaded, quorumOverride);
|
|
101
|
+
gateVerdict = aggregate.verdict;
|
|
102
|
+
disagreements = aggregate.disagreements;
|
|
19
103
|
}
|
|
104
|
+
// Cursor finding (report.ts:981) — persist the demoted findings on
|
|
105
|
+
// the artifact so operators auditing the on-disk JSON see the
|
|
106
|
+
// demotion without re-running the aggregator. Always emit `[]` on
|
|
107
|
+
// complete artifacts so consumers can branch on `.length` without
|
|
108
|
+
// an undefined check; omit on pending/error artifacts (no
|
|
109
|
+
// aggregation ran, no policy outcome to record).
|
|
20
110
|
const artifact = {
|
|
21
111
|
version: 2,
|
|
22
112
|
status,
|
|
@@ -30,9 +120,21 @@ export function buildAggregate(inputs) {
|
|
|
30
120
|
criticResults,
|
|
31
121
|
createdAt,
|
|
32
122
|
...(gateVerdict !== undefined ? { gateVerdict } : {}),
|
|
123
|
+
...(status === "complete" ? { disagreements } : {}),
|
|
33
124
|
};
|
|
34
125
|
return artifact;
|
|
35
126
|
}
|
|
127
|
+
function aggregateOutcome(results, blockingSeverities, loaded, quorumOverride) {
|
|
128
|
+
if (loaded.config.aggregation.policy === "min-complete-quorum") {
|
|
129
|
+
const quorum = quorumOverride ?? loaded.config.aggregation.quorum;
|
|
130
|
+
const outcome = quorumAggregateVerdict(results, blockingSeverities, quorum ?? 2, loaded.config.aggregation.unilateralVetoRules);
|
|
131
|
+
return { verdict: outcome.verdict, disagreements: outcome.disagreements };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
verdict: aggregateVerdict(results, blockingSeverities, loaded, quorumOverride),
|
|
135
|
+
disagreements: [],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
36
138
|
function aggregateVerdict(results, blockingSeverities, loaded, quorumOverride) {
|
|
37
139
|
// Cycle 322.3 — dispatch on policy. `min-complete-quorum` uses the
|
|
38
140
|
// quorum aggregator below; everything else falls through to the
|
|
@@ -45,11 +147,14 @@ function aggregateVerdict(results, blockingSeverities, loaded, quorumOverride) {
|
|
|
45
147
|
// report CHANGES_REQUESTED (uses root quorum=2). Codex P2 caught
|
|
46
148
|
// this divergence on PR #1468.
|
|
47
149
|
const quorum = quorumOverride ?? loaded.config.aggregation.quorum;
|
|
150
|
+
// Issue dark-factory-platform#112 — thread the optional
|
|
151
|
+
// `unilateralVetoRules` so the persisted artifact's verdict honors
|
|
152
|
+
// the same corroboration policy the runtime gate evaluator does.
|
|
48
153
|
// Schema validation guarantees `quorum` is set when the policy
|
|
49
154
|
// is `min-complete-quorum`; the defensive fallback to 2 is for
|
|
50
155
|
// hand-constructed configs in tests (the parser would already
|
|
51
156
|
// have rejected a real on-disk config without quorum).
|
|
52
|
-
return quorumAggregateVerdict(results, blockingSeverities, quorum ?? 2).verdict;
|
|
157
|
+
return quorumAggregateVerdict(results, blockingSeverities, quorum ?? 2, loaded.config.aggregation.unilateralVetoRules).verdict;
|
|
53
158
|
}
|
|
54
159
|
// Cycle 322.2 Component 4 — `required` flag threading.
|
|
55
160
|
//
|
|
@@ -121,16 +226,115 @@ export function isCriticCompleted(r) {
|
|
|
121
226
|
* CHANGES_REQUESTED verdict) blocks the gate regardless of how many
|
|
122
227
|
* other critics approved. Returns `false` for non-completed critics
|
|
123
228
|
* — only a completed critic can veto.
|
|
229
|
+
*
|
|
230
|
+
* Issue dark-factory-platform#112 — when `corroborationCtx` is
|
|
231
|
+
* supplied AND the critic's only veto trigger is a blocking finding
|
|
232
|
+
* carrying one of the configured corroboration-required flags (e.g.
|
|
233
|
+
* `selfInconsistent: true`), the function checks whether ANOTHER
|
|
234
|
+
* critic raises a blocking finding on the same file within
|
|
235
|
+
* `rules.requireCorroborationOnHunkRadius` lines. If not, the
|
|
236
|
+
* finding is demoted to a `critic_disagreement` note (pushed to
|
|
237
|
+
* `disagreementSink`) and DOES NOT contribute to a veto. Findings
|
|
238
|
+
* without a flag still veto unconditionally — the safety net is
|
|
239
|
+
* intact for findings the critic can defend.
|
|
240
|
+
*
|
|
241
|
+
* Note: a critic that vetoes via `CHANGES_REQUESTED` verdict OR
|
|
242
|
+
* `requiresHumanJudgment` is NOT subject to corroboration — those
|
|
243
|
+
* are coarser-grained signals than per-finding flags and the spec
|
|
244
|
+
* narrows the rule to per-finding flag triggers only.
|
|
124
245
|
*/
|
|
125
|
-
export function criticVetoesGate(r, blockingSeverities) {
|
|
246
|
+
export function criticVetoesGate(r, blockingSeverities, corroborationCtx) {
|
|
126
247
|
if (!isCriticCompleted(r))
|
|
127
248
|
return false;
|
|
128
|
-
|
|
129
|
-
|
|
249
|
+
// requiresHumanJudgment is a coarser-grained signal than per-finding
|
|
250
|
+
// flags; the spec narrows corroboration to per-finding triggers
|
|
251
|
+
// only. A critic that flags requiresHumanJudgment vetoes
|
|
252
|
+
// unconditionally regardless of policy.
|
|
130
253
|
if (r.requiresHumanJudgment)
|
|
131
254
|
return true;
|
|
132
|
-
|
|
255
|
+
// Back-compat path: no policy → check verdict OR any blocking
|
|
256
|
+
// finding (the pre-#112 §11 semantic).
|
|
257
|
+
if (corroborationCtx === undefined) {
|
|
258
|
+
if (r.verdict === "CHANGES_REQUESTED")
|
|
259
|
+
return true;
|
|
260
|
+
return hasBlockingFinding(r.findings, blockingSeverities);
|
|
261
|
+
}
|
|
262
|
+
// Issue dark-factory-platform#112 — policy-aware veto evaluation.
|
|
263
|
+
// Walk every blocking finding and partition into:
|
|
264
|
+
// - `anyUnflaggedBlocking`: the safety net — any finding the
|
|
265
|
+
// critic can defend stands as a veto.
|
|
266
|
+
// - `anyCorroboratedFlagged`: flagged AND another critic raised a
|
|
267
|
+
// same-file blocker within radius → veto stands.
|
|
268
|
+
// - `demotions`: flagged AND uncorroborated → recorded as
|
|
269
|
+
// `critic_disagreement` notes; do NOT contribute to veto.
|
|
270
|
+
//
|
|
271
|
+
// A critic with `verdict === "CHANGES_REQUESTED"` is itself subject
|
|
272
|
+
// to demotion under policy WHEN every blocking finding is demoted
|
|
273
|
+
// AND the verdict is downstream of those findings (heuristic: if no
|
|
274
|
+
// unflagged or corroborated blocker remains, the CR verdict is the
|
|
275
|
+
// critic's downstream summary of the now-demoted finding(s)). This
|
|
276
|
+
// is the spec's "verdict flips from CHANGES_REQUESTED to APPROVED"
|
|
277
|
+
// case from the DoD. A critic with CR and NO blocking findings at
|
|
278
|
+
// all retains its veto unconditionally — the verdict in that case
|
|
279
|
+
// is verdict-only with no per-finding flag to demote.
|
|
280
|
+
const { allResults, rules, disagreementSink } = corroborationCtx;
|
|
281
|
+
let anyUnflaggedBlocking = false;
|
|
282
|
+
let anyCorroboratedFlagged = false;
|
|
283
|
+
let anyBlockingFinding = false;
|
|
284
|
+
const demotions = [];
|
|
285
|
+
for (const f of r.findings) {
|
|
286
|
+
if (!blockingSeverities.includes(f.severity))
|
|
287
|
+
continue;
|
|
288
|
+
anyBlockingFinding = true;
|
|
289
|
+
const flagged = findingCarriesCorroborationFlag(f, rules.requireCorroborationFor);
|
|
290
|
+
if (!flagged) {
|
|
291
|
+
anyUnflaggedBlocking = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const corroborated = isCorroboratedByOtherCritic(f, r.criticId, allResults, blockingSeverities, rules.requireCorroborationOnHunkRadius);
|
|
295
|
+
if (corroborated) {
|
|
296
|
+
anyCorroboratedFlagged = true;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
// Flagged AND uncorroborated → demote to a disagreement note.
|
|
300
|
+
if (f.file) {
|
|
301
|
+
demotions.push({
|
|
302
|
+
criticId: r.criticId,
|
|
303
|
+
file: f.file,
|
|
304
|
+
...(f.line !== undefined ? { line: f.line } : {}),
|
|
305
|
+
severity: f.severity,
|
|
306
|
+
// The first flag the finding actually carries wins for note
|
|
307
|
+
// attribution; in practice today only `self_inconsistent` is
|
|
308
|
+
// a registered key so this is a single-element search.
|
|
309
|
+
flag: rules.requireCorroborationFor.find((name) => {
|
|
310
|
+
const key = FLAG_TO_FINDING_KEY[name];
|
|
311
|
+
return key !== undefined && f[key] === true;
|
|
312
|
+
}) ?? rules.requireCorroborationFor[0],
|
|
313
|
+
evidence: f.evidence,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Veto sustained when any defendable signal remains:
|
|
318
|
+
// - An unflagged blocker (the safety net).
|
|
319
|
+
// - A corroborated flagged blocker (another critic agrees).
|
|
320
|
+
// - A CHANGES_REQUESTED verdict that's NOT downstream of the
|
|
321
|
+
// now-demoted findings — i.e. the critic returned CR without any
|
|
322
|
+
// blocking finding to attribute it to (verdict-only block, the
|
|
323
|
+
// coarser §11 signal). That CR survives policy because there's
|
|
324
|
+
// no per-finding flag to demote it through.
|
|
325
|
+
if (anyUnflaggedBlocking || anyCorroboratedFlagged) {
|
|
326
|
+
if (disagreementSink)
|
|
327
|
+
disagreementSink.push(...demotions);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
if (r.verdict === "CHANGES_REQUESTED" && !anyBlockingFinding) {
|
|
331
|
+
// Verdict-only block, no blockers to demote → veto sustained.
|
|
133
332
|
return true;
|
|
333
|
+
}
|
|
334
|
+
// All blockers demoted (and either APPROVED verdict OR
|
|
335
|
+
// CHANGES_REQUESTED-downstream-of-blockers) → no veto.
|
|
336
|
+
if (disagreementSink)
|
|
337
|
+
disagreementSink.push(...demotions);
|
|
134
338
|
return false;
|
|
135
339
|
}
|
|
136
340
|
/**
|
|
@@ -154,15 +358,29 @@ export function criticVetoesGate(r, blockingSeverities) {
|
|
|
154
358
|
* property: an outage that knocks out two critics doesn't paralyze
|
|
155
359
|
* the third critic's BLOCKER finding.
|
|
156
360
|
*/
|
|
157
|
-
export function quorumAggregateVerdict(results, blockingSeverities, quorum) {
|
|
361
|
+
export function quorumAggregateVerdict(results, blockingSeverities, quorum, unilateralVetoRules) {
|
|
158
362
|
const completed = results.filter(isCriticCompleted);
|
|
159
|
-
const
|
|
363
|
+
const disagreements = [];
|
|
364
|
+
// Issue dark-factory-platform#112 — when the policy is set, thread
|
|
365
|
+
// it through every `criticVetoesGate` call so a single uncorroborated
|
|
366
|
+
// flagged finding doesn't sweep a veto. The disagreement sink is
|
|
367
|
+
// shared across critics so the runner sees all demotions in one
|
|
368
|
+
// pass.
|
|
369
|
+
const ctx = unilateralVetoRules !== undefined
|
|
370
|
+
? {
|
|
371
|
+
allResults: results,
|
|
372
|
+
rules: unilateralVetoRules,
|
|
373
|
+
disagreementSink: disagreements,
|
|
374
|
+
}
|
|
375
|
+
: undefined;
|
|
376
|
+
const vetoer = completed.find((r) => ctx ? criticVetoesGate(r, blockingSeverities, ctx) : criticVetoesGate(r, blockingSeverities));
|
|
160
377
|
if (vetoer) {
|
|
161
378
|
return {
|
|
162
379
|
verdict: "CHANGES_REQUESTED",
|
|
163
380
|
reason: "veto",
|
|
164
381
|
completedCount: completed.length,
|
|
165
382
|
totalCount: results.length,
|
|
383
|
+
disagreements,
|
|
166
384
|
};
|
|
167
385
|
}
|
|
168
386
|
if (completed.length < quorum) {
|
|
@@ -171,12 +389,40 @@ export function quorumAggregateVerdict(results, blockingSeverities, quorum) {
|
|
|
171
389
|
reason: "quorum_unmet",
|
|
172
390
|
completedCount: completed.length,
|
|
173
391
|
totalCount: results.length,
|
|
392
|
+
disagreements,
|
|
174
393
|
};
|
|
175
394
|
}
|
|
176
395
|
// Quorum met and no veto — vote the majority of completed critics.
|
|
177
396
|
// Without veto, every completed critic has APPROVED OR (no verdict
|
|
178
397
|
// — schema impossible, but defensive). Count APPROVED vs other.
|
|
179
|
-
|
|
398
|
+
//
|
|
399
|
+
// Issue dark-factory-platform#112 — when a corroboration policy is
|
|
400
|
+
// active and a critic's CHANGES_REQUESTED verdict is downstream of
|
|
401
|
+
// a now-demoted blocker (every blocking finding the critic produced
|
|
402
|
+
// was demoted under the policy), treat that critic as effectively
|
|
403
|
+
// APPROVED for majority counting. Without this, the critic that
|
|
404
|
+
// produced the demoted blocker would still flip the majority to
|
|
405
|
+
// CHANGES_REQUESTED via its CR verdict, which would undo the
|
|
406
|
+
// demotion the policy just performed. This is the spec's DoD case:
|
|
407
|
+
// "verdict flips from CHANGES_REQUESTED to APPROVED after the
|
|
408
|
+
// probe-tagged blocker is demoted, no other critic finds a
|
|
409
|
+
// corroborating blocker on the same file within radius."
|
|
410
|
+
const effectivelyApproved = (r) => {
|
|
411
|
+
if (r.verdict === "APPROVED")
|
|
412
|
+
return true;
|
|
413
|
+
if (ctx === undefined)
|
|
414
|
+
return false;
|
|
415
|
+
// If the critic returned CR with at least one blocker and every
|
|
416
|
+
// blocker was demoted to a disagreement note attributed to this
|
|
417
|
+
// critic, the CR verdict is downstream of the now-demoted
|
|
418
|
+
// finding(s).
|
|
419
|
+
const blockers = r.findings.filter((f) => blockingSeverities.includes(f.severity));
|
|
420
|
+
if (blockers.length === 0)
|
|
421
|
+
return false;
|
|
422
|
+
const demotedForThisCritic = disagreements.filter((d) => d.criticId === r.criticId).length;
|
|
423
|
+
return demotedForThisCritic >= blockers.length;
|
|
424
|
+
};
|
|
425
|
+
const approveCount = completed.filter(effectivelyApproved).length;
|
|
180
426
|
const changesCount = completed.length - approveCount;
|
|
181
427
|
// Ties favor CHANGES_REQUESTED (conservative). With 0 changes, all
|
|
182
428
|
// approved → APPROVED. With any changes >= approveCount → CHANGES_REQUESTED.
|
|
@@ -190,6 +436,7 @@ export function quorumAggregateVerdict(results, blockingSeverities, quorum) {
|
|
|
190
436
|
reason: "majority",
|
|
191
437
|
completedCount: completed.length,
|
|
192
438
|
totalCount: results.length,
|
|
439
|
+
disagreements,
|
|
193
440
|
};
|
|
194
441
|
}
|
|
195
442
|
/**
|
|
@@ -279,6 +526,91 @@ export function buildCriticReport(artifact, jsonPath) {
|
|
|
279
526
|
degraded,
|
|
280
527
|
};
|
|
281
528
|
}
|
|
529
|
+
export function buildZeroEvidenceDiagnostic(artifact, jsonPath, options = {}) {
|
|
530
|
+
const results = artifact.criticResults;
|
|
531
|
+
const totalCount = results.length;
|
|
532
|
+
const completedCount = results.filter((r) => r.status === "complete").length;
|
|
533
|
+
const isZeroEvidence = totalCount > 0 && completedCount === 0;
|
|
534
|
+
if (!isZeroEvidence) {
|
|
535
|
+
return { isZeroEvidence, stderr: "" };
|
|
536
|
+
}
|
|
537
|
+
const classified = results.map((r) => classifyCritic(r));
|
|
538
|
+
const lines = [
|
|
539
|
+
`df: review COMPLETED with 0/${totalCount} critics producing evidence — gate will block at push time.`,
|
|
540
|
+
`df: fix one of:`,
|
|
541
|
+
];
|
|
542
|
+
// Top-line config remediation when the caller signaled the profiles
|
|
543
|
+
// block is missing entirely (sage-blueprint seed bug shape).
|
|
544
|
+
if (options.configHasProfiles === false) {
|
|
545
|
+
lines.push(`df: - add a 'profiles' block to .agent-review/config.json (mirror sage3c's pattern; see docs/CONSUMER-ADOPTION.md)`);
|
|
546
|
+
}
|
|
547
|
+
// Per-critic remediation hints. Use a Set to dedupe identical hints
|
|
548
|
+
// (e.g., two critics from the same family both missing the same env).
|
|
549
|
+
const emittedHints = new Set();
|
|
550
|
+
for (const c of classified) {
|
|
551
|
+
const hint = `df: - ${c.criticId}: ${c.remediation}`;
|
|
552
|
+
if (emittedHints.has(hint))
|
|
553
|
+
continue;
|
|
554
|
+
emittedHints.add(hint);
|
|
555
|
+
lines.push(hint);
|
|
556
|
+
}
|
|
557
|
+
lines.push(`df: details: ${jsonPath}`);
|
|
558
|
+
return {
|
|
559
|
+
isZeroEvidence,
|
|
560
|
+
stderr: `${lines.join("\n")}\n`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function classifyCritic(result) {
|
|
564
|
+
const message = result.error?.message ?? "";
|
|
565
|
+
const code = result.error?.code ?? "";
|
|
566
|
+
// Codex "no auth source pinned" — the documented failure when the
|
|
567
|
+
// profile lacks the per-critic `auth` entry. The codex-sdk adapter
|
|
568
|
+
// raises this with a stable message prefix.
|
|
569
|
+
if (/no auth source pinned/i.test(message)) {
|
|
570
|
+
return {
|
|
571
|
+
criticId: result.criticId,
|
|
572
|
+
classification: "no_auth_codex_unpinned",
|
|
573
|
+
remediation: `pin profiles.<name>.auth["${result.criticId}"] to "chatgpt" (and run \`codex login\`) or "api" (and set CODEX_API_KEY)`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// Env-var-missing — the cursor/gemini/grok/codex API-key paths use a
|
|
577
|
+
// stable "<KEY> is not set" message in their auth probes.
|
|
578
|
+
const envMatch = message.match(/([A-Z][A-Z0-9_]*_API_KEY|[A-Z][A-Z0-9_]*_TOKEN)\s+is\s+not\s+set/);
|
|
579
|
+
if (envMatch && envMatch[1]) {
|
|
580
|
+
const envName = envMatch[1];
|
|
581
|
+
return {
|
|
582
|
+
criticId: result.criticId,
|
|
583
|
+
classification: "no_auth_env_missing",
|
|
584
|
+
remediation: `export ${envName}=... (or add it to your Doppler scope)`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
// Transport / capacity errors — these surface as SDK error codes
|
|
588
|
+
// (`rate_limited`, `capacity_exceeded`, `timeout`, `service_unavailable`).
|
|
589
|
+
if (/^(rate_limited|capacity_exceeded|timeout|service_unavailable|server_error|unavailable)$/i.test(code) ||
|
|
590
|
+
/capacity_exceeded|rate.?limit|service unavailable/i.test(message)) {
|
|
591
|
+
return {
|
|
592
|
+
criticId: result.criticId,
|
|
593
|
+
classification: "transport_error",
|
|
594
|
+
remediation: `transport/rate-limit error — retry the commit; if persistent, check vendor status`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
// Schema violations — adapter returned data that doesn't match the
|
|
598
|
+
// expected schema (typically after a vendor model bump).
|
|
599
|
+
if (/^(schema_violation|invalid_response|malformed_response)$/i.test(code) ||
|
|
600
|
+
/schema_violation|malformed verdict|malformed response/i.test(message)) {
|
|
601
|
+
return {
|
|
602
|
+
criticId: result.criticId,
|
|
603
|
+
classification: "schema_violation",
|
|
604
|
+
remediation: `schema violation — upgrade the @momentiq/dark-factory-cli adapter version`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
// Generic fallback — point the operator at `df doctor` for triage.
|
|
608
|
+
return {
|
|
609
|
+
criticId: result.criticId,
|
|
610
|
+
classification: "unknown",
|
|
611
|
+
remediation: `unclassified error — run \`df doctor --profile <name>\` for per-critic triage`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
282
614
|
// JSON is the authoritative gate-input; markdown is a presentation artifact.
|
|
283
615
|
// The write order is JSON-first, markdown best-effort, so a broken markdown
|
|
284
616
|
// path (e.g., a previously-created directory at the markdown filename) cannot
|
|
@@ -398,7 +730,14 @@ export function renderMarkdown(artifact) {
|
|
|
398
730
|
for (const f of subset) {
|
|
399
731
|
lines.push("");
|
|
400
732
|
const tag = f.manifestoSection ? ` ${f.manifestoSection}` : "";
|
|
401
|
-
|
|
733
|
+
// Cursor finding (report.ts:981) — surface the
|
|
734
|
+
// self-consistency-probe verdict alongside the location so a
|
|
735
|
+
// reader scanning the markdown immediately sees which
|
|
736
|
+
// blockers were probe-flagged. The aggregator may have
|
|
737
|
+
// demoted these to disagreement notes — see the
|
|
738
|
+
// "Demoted findings" section at the end of the artifact.
|
|
739
|
+
const probeTag = f.selfInconsistent === true ? " [self-inconsistent]" : "";
|
|
740
|
+
lines.push(`- **${f.category}${tag}** — \`${f.file ?? "(no file)"}\`${f.line !== undefined ? `:${f.line}` : ""}${f.symbol ? ` (\`${f.symbol}\`)` : ""}${probeTag}`);
|
|
402
741
|
lines.push(` - Evidence: ${f.evidence}`);
|
|
403
742
|
lines.push(` - Impact: ${f.impact}`);
|
|
404
743
|
lines.push(` - Required fix: ${f.requiredFix}`);
|
|
@@ -425,6 +764,24 @@ export function renderMarkdown(artifact) {
|
|
|
425
764
|
}
|
|
426
765
|
lines.push("");
|
|
427
766
|
}
|
|
767
|
+
// Cursor finding (report.ts:981) — surface the aggregator's
|
|
768
|
+
// demoted findings so a reader of the per-SHA markdown sees the
|
|
769
|
+
// policy outcome (which blockers became disagreement notes and
|
|
770
|
+
// why). Omitted entirely when no demotions occurred (the common
|
|
771
|
+
// case for runs without `unilateralVetoRules` set or for runs
|
|
772
|
+
// where every flagged blocker corroborated).
|
|
773
|
+
if (artifact.disagreements && artifact.disagreements.length > 0) {
|
|
774
|
+
lines.push("## Demoted findings");
|
|
775
|
+
lines.push("");
|
|
776
|
+
lines.push("Findings the aggregator demoted from a unilateral veto to an informational `critic_disagreement` note under `unilateralVetoRules.requireCorroborationFor`.");
|
|
777
|
+
lines.push("");
|
|
778
|
+
for (const d of artifact.disagreements) {
|
|
779
|
+
const loc = d.line !== undefined ? `${d.file}:${d.line}` : d.file;
|
|
780
|
+
lines.push(`- **${d.criticId}** — \`${loc}\` (${d.severity}, flag: \`${d.flag}\`)`);
|
|
781
|
+
lines.push(` - Evidence: ${d.evidence}`);
|
|
782
|
+
}
|
|
783
|
+
lines.push("");
|
|
784
|
+
}
|
|
428
785
|
return `${lines.join("\n")}\n`;
|
|
429
786
|
}
|
|
430
787
|
export async function writePending(loaded, inputs) {
|