dsh-doublecheck 0.5.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 +65 -0
- package/LICENSE +201 -0
- package/README.es.md +252 -0
- package/README.hi.md +252 -0
- package/README.md +268 -0
- package/README.pt.md +252 -0
- package/README.zh.md +266 -0
- package/cordis.patch.yml +19 -0
- package/lib/domain/evidence.d.ts +100 -0
- package/lib/domain/evidence.js +180 -0
- package/lib/domain/projection.d.ts +60 -0
- package/lib/domain/projection.js +132 -0
- package/lib/domain/report.d.ts +93 -0
- package/lib/domain/report.js +266 -0
- package/lib/domain/stages.d.ts +98 -0
- package/lib/domain/stages.js +192 -0
- package/lib/domain/vagueness.d.ts +25 -0
- package/lib/domain/vagueness.js +56 -0
- package/lib/domain/vocabulary.d.ts +50 -0
- package/lib/domain/vocabulary.js +16 -0
- package/lib/events.d.ts +155 -0
- package/lib/events.js +15 -0
- package/lib/grill/index.d.ts +49 -0
- package/lib/grill/index.js +495 -0
- package/lib/grill/provider.d.ts +56 -0
- package/lib/grill/provider.js +189 -0
- package/lib/guard/command.d.ts +47 -0
- package/lib/guard/command.js +132 -0
- package/lib/guard/index.d.ts +111 -0
- package/lib/guard/index.js +516 -0
- package/lib/guard/prose.d.ts +34 -0
- package/lib/guard/prose.js +80 -0
- package/lib/guard/review.d.ts +67 -0
- package/lib/guard/review.js +184 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +15 -0
- package/lib/invariant.d.ts +56 -0
- package/lib/invariant.js +112 -0
- package/lib/types.d.ts +39 -0
- package/lib/types.js +18 -0
- package/package.json +109 -0
- package/skills/delivery-proof/SKILL.md +52 -0
- package/skills/delivery-review/SKILL.md +49 -0
- package/skills/grill-requirements/SKILL.md +98 -0
- package/skills/red-green-tdd/SKILL.md +57 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversary review: a forked critic subagent audits the delivery against the
|
|
3
|
+
* committed spec.
|
|
4
|
+
*
|
|
5
|
+
* The critic is a one-shot `ctx.subagents` run on the configured provider
|
|
6
|
+
* (default `fork`): the child inherits the parent session's completed-turn
|
|
7
|
+
* prefix, so it sees the `doublecheck_spec` record, the red/green test
|
|
8
|
+
* evidence, and every edit without any extra plumbing. A structured output
|
|
9
|
+
* schema forces the findings shape; `agentOptions.model` routes the critic to
|
|
10
|
+
* `adversaryModel` when one is configured. The critic may read (never mutate)
|
|
11
|
+
* through an explicit tool allowlist.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-doublecheck/guard/review
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
17
|
+
import type { UserMessage } from '@deepseek-ai/dsh-llm';
|
|
18
|
+
import type { ObjectJsonSchema } from '@deepseek-ai/dsh-tools';
|
|
19
|
+
import type { ReviewFinding, ReviewVerdict } from '../domain/vocabulary.ts';
|
|
20
|
+
import { type GuardProse, type ProseLanguage } from './prose.ts';
|
|
21
|
+
/** The adversary knobs the review runner reads. */
|
|
22
|
+
export interface AdversaryConfig {
|
|
23
|
+
adversaryProvider: string;
|
|
24
|
+
adversaryModel: string | null;
|
|
25
|
+
adversaryMaxFindings: number;
|
|
26
|
+
adversaryTools: string[];
|
|
27
|
+
adversaryTimeoutMs: number;
|
|
28
|
+
/** Language of the injected review prose. */
|
|
29
|
+
language: ProseLanguage;
|
|
30
|
+
}
|
|
31
|
+
/** The settled outcome of one review run, ready for injection. */
|
|
32
|
+
export interface ReviewOutcome {
|
|
33
|
+
verdict: ReviewVerdict;
|
|
34
|
+
findings: ReviewFinding[];
|
|
35
|
+
/** Model-facing prose for the injection channel. */
|
|
36
|
+
text: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Structured output the critic must satisfy: a findings list whose entries
|
|
40
|
+
* carry a severity, a one-line title, and the supporting detail.
|
|
41
|
+
*/
|
|
42
|
+
export declare const REVIEW_OUTPUT_SCHEMA: ObjectJsonSchema;
|
|
43
|
+
/** Injected when the critic found nothing supportable (English contract text). */
|
|
44
|
+
export declare const CLEAN_TEXT: string;
|
|
45
|
+
/** Render structured findings as the model-facing review text. */
|
|
46
|
+
export declare function renderFindings(findings: readonly ReviewFinding[], prose?: GuardProse, totalCount?: number): string;
|
|
47
|
+
/**
|
|
48
|
+
* Run one adversary review for the given agent and settle it to an
|
|
49
|
+
* injectable outcome. Failures of the review mechanism itself (provider
|
|
50
|
+
* missing, run aborted, model failure) settle as `unavailable` with an
|
|
51
|
+
* honest notice instead of blocking the turn boundary on a broken critic.
|
|
52
|
+
* @param ctx - plugin context carrying the subagents seam.
|
|
53
|
+
* @param config - the adversary knobs.
|
|
54
|
+
* @param agent - the reviewed agent; its session seeds the forked critic.
|
|
55
|
+
* @param turnSignal - the turn's abort signal; cancelling the turn cancels
|
|
56
|
+
* the review instead of letting it run out the timeout.
|
|
57
|
+
* @returns the settled review outcome.
|
|
58
|
+
*/
|
|
59
|
+
export declare function runAdversaryReview(ctx: Context, config: AdversaryConfig, agent: Agent, turnSignal?: AbortSignal): Promise<ReviewOutcome>;
|
|
60
|
+
/**
|
|
61
|
+
* The injected review message: model-facing prose with the structured
|
|
62
|
+
* findings riding the durable `doublecheck-review` message source, so the
|
|
63
|
+
* doublecheck report can fold the record without re-parsing the prose.
|
|
64
|
+
* @param outcome - the settled review outcome.
|
|
65
|
+
* @returns the injection-ready user message.
|
|
66
|
+
*/
|
|
67
|
+
export declare function reviewInjection(outcome: ReviewOutcome): UserMessage;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversary review: a forked critic subagent audits the delivery against the
|
|
3
|
+
* committed spec.
|
|
4
|
+
*
|
|
5
|
+
* The critic is a one-shot `ctx.subagents` run on the configured provider
|
|
6
|
+
* (default `fork`): the child inherits the parent session's completed-turn
|
|
7
|
+
* prefix, so it sees the `doublecheck_spec` record, the red/green test
|
|
8
|
+
* evidence, and every edit without any extra plumbing. A structured output
|
|
9
|
+
* schema forces the findings shape; `agentOptions.model` routes the critic to
|
|
10
|
+
* `adversaryModel` when one is configured. The critic may read (never mutate)
|
|
11
|
+
* through an explicit tool allowlist.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-doublecheck/guard/review
|
|
14
|
+
*/
|
|
15
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
16
|
+
import { PROSE } from "./prose.js";
|
|
17
|
+
/**
|
|
18
|
+
* Structured output the critic must satisfy: a findings list whose entries
|
|
19
|
+
* carry a severity, a one-line title, and the supporting detail.
|
|
20
|
+
*/
|
|
21
|
+
export const REVIEW_OUTPUT_SCHEMA = {
|
|
22
|
+
type: 'object',
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
required: ['findings'],
|
|
25
|
+
properties: {
|
|
26
|
+
findings: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
required: ['severity', 'title', 'detail'],
|
|
32
|
+
properties: {
|
|
33
|
+
severity: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['blocker', 'major', 'minor', 'info'],
|
|
36
|
+
},
|
|
37
|
+
title: { type: 'string' },
|
|
38
|
+
detail: { type: 'string' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The critic's task, delivered as the forked child's user message. The child
|
|
46
|
+
* already holds the parent history; this prompt only sets the adversarial
|
|
47
|
+
* stance and the answer discipline.
|
|
48
|
+
*/
|
|
49
|
+
const CRITIC_TASK = 'You are the delivery reviewer for this software-engineering session. The '
|
|
50
|
+
+ 'conversation you inherited contains a requirements spec recorded with the '
|
|
51
|
+
+ 'doublecheck_spec tool (six dimensions: goal, scope, acceptance criteria, '
|
|
52
|
+
+ 'failure modes, priorities, non-goals), followed by the implementation '
|
|
53
|
+
+ 'work and its test evidence. Assume the delivery FAILS its own spec. Hunt '
|
|
54
|
+
+ 'for the strongest objections you can actually support from this session: '
|
|
55
|
+
+ 'dimensions the work did not meet, acceptance criteria with no evidence, '
|
|
56
|
+
+ 'scope or non-goal violations, failure modes left unhandled. Answer '
|
|
57
|
+
+ 'through the required structured output, one entry per objection, citing '
|
|
58
|
+
+ 'what in the session supports it. If — and only if — the evidence '
|
|
59
|
+
+ 'genuinely satisfies every dimension, return an empty findings list. Do '
|
|
60
|
+
+ 'not invent objections; the empty answer is correct when nothing is wrong.';
|
|
61
|
+
/** Injected when the critic found nothing supportable (English contract text). */
|
|
62
|
+
export const CLEAN_TEXT = PROSE.en.reviewClean;
|
|
63
|
+
/** Render structured findings as the model-facing review text. */
|
|
64
|
+
export function renderFindings(findings, prose = PROSE.en, totalCount = findings.length) {
|
|
65
|
+
const lines = [
|
|
66
|
+
prose.reviewFindingsHeader(findings.length),
|
|
67
|
+
'',
|
|
68
|
+
];
|
|
69
|
+
for (const finding of findings) {
|
|
70
|
+
lines.push(`- [${finding.severity}] ${finding.title}`);
|
|
71
|
+
lines.push(` ${finding.detail}`);
|
|
72
|
+
}
|
|
73
|
+
if (totalCount > findings.length) {
|
|
74
|
+
lines.push(`… ${totalCount - findings.length} further objection(s) held back by adversaryMaxFindings`);
|
|
75
|
+
}
|
|
76
|
+
lines.push('', prose.reviewFindingsFooter);
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run one adversary review for the given agent and settle it to an
|
|
81
|
+
* injectable outcome. Failures of the review mechanism itself (provider
|
|
82
|
+
* missing, run aborted, model failure) settle as `unavailable` with an
|
|
83
|
+
* honest notice instead of blocking the turn boundary on a broken critic.
|
|
84
|
+
* @param ctx - plugin context carrying the subagents seam.
|
|
85
|
+
* @param config - the adversary knobs.
|
|
86
|
+
* @param agent - the reviewed agent; its session seeds the forked critic.
|
|
87
|
+
* @param turnSignal - the turn's abort signal; cancelling the turn cancels
|
|
88
|
+
* the review instead of letting it run out the timeout.
|
|
89
|
+
* @returns the settled review outcome.
|
|
90
|
+
*/
|
|
91
|
+
export async function runAdversaryReview(ctx, config, agent, turnSignal) {
|
|
92
|
+
const prose = PROSE[config.language];
|
|
93
|
+
const subagents = ctx.get('subagents');
|
|
94
|
+
if (subagents === undefined) {
|
|
95
|
+
ctx.logger.warn('dsh-doublecheck: adversary review skipped: the ctx.subagents seam is not mounted');
|
|
96
|
+
return { verdict: 'unavailable', findings: [], text: prose.reviewUnavailableSeam };
|
|
97
|
+
}
|
|
98
|
+
const timeout = AbortSignal.timeout(config.adversaryTimeoutMs);
|
|
99
|
+
const signal = turnSignal === undefined ? timeout : AbortSignal.any([turnSignal, timeout]);
|
|
100
|
+
let run;
|
|
101
|
+
try {
|
|
102
|
+
run = await subagents.start(config.adversaryProvider, {
|
|
103
|
+
label: 'doublecheck-adversary',
|
|
104
|
+
prompt: [{ type: 'text', text: CRITIC_TASK }],
|
|
105
|
+
parent: agent,
|
|
106
|
+
signal,
|
|
107
|
+
...config.adversaryModel !== null ? { agentOptions: { model: config.adversaryModel } } : {},
|
|
108
|
+
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
109
|
+
toolFilter: { allow: config.adversaryTools },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// Provider absence, capability rejection, or a child that could not be
|
|
114
|
+
// composed: the review itself never ran, and the notice says so.
|
|
115
|
+
return { verdict: 'unavailable', findings: [], text: prose.reviewUnavailableFailed(String(error)) };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const result = await settleResult(run, signal, prose, config.adversaryMaxFindings);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
await run.dispose();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Await one critic run; a rejection settles as an honest unavailable notice. */
|
|
126
|
+
async function settleResult(run, signal, prose, maxFindings) {
|
|
127
|
+
let result;
|
|
128
|
+
try {
|
|
129
|
+
result = await run.result;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (signal.aborted) {
|
|
133
|
+
return { verdict: 'unavailable', findings: [], text: prose.reviewUnavailableStopped('aborted') };
|
|
134
|
+
}
|
|
135
|
+
// A broken critic must not throw into the turn-stopping chain.
|
|
136
|
+
return { verdict: 'unavailable', findings: [], text: prose.reviewUnavailableFailed(String(error)) };
|
|
137
|
+
}
|
|
138
|
+
if (result.stopReason !== 'completed') {
|
|
139
|
+
return {
|
|
140
|
+
verdict: 'unavailable',
|
|
141
|
+
findings: [],
|
|
142
|
+
text: prose.reviewUnavailableStopped(result.stopReason),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// The seam validates `structured` against the requested schema before a
|
|
146
|
+
// completed run settles, so the typed reading needs no re-validation.
|
|
147
|
+
const structured = result.structured;
|
|
148
|
+
const raw = structured?.findings;
|
|
149
|
+
if (!Array.isArray(raw)) {
|
|
150
|
+
return { verdict: 'unavailable', findings: [], text: outputText(result.output, prose.reviewUnavailableNoFindings) };
|
|
151
|
+
}
|
|
152
|
+
const findings = raw.slice(0, maxFindings);
|
|
153
|
+
if (findings.length === 0)
|
|
154
|
+
return { verdict: 'clean', findings: [], text: prose.reviewClean };
|
|
155
|
+
return { verdict: 'findings', findings, text: renderFindings(findings, prose, raw.length) };
|
|
156
|
+
}
|
|
157
|
+
/** Join a result's content blocks, with a fallback line when they are empty. */
|
|
158
|
+
function outputText(content, fallback) {
|
|
159
|
+
const parts = [];
|
|
160
|
+
for (const block of content) {
|
|
161
|
+
if (block.type === 'text')
|
|
162
|
+
parts.push(block.text);
|
|
163
|
+
}
|
|
164
|
+
const text = parts.join('\n').trim();
|
|
165
|
+
return text.length > 0 ? text : fallback;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The injected review message: model-facing prose with the structured
|
|
169
|
+
* findings riding the durable `doublecheck-review` message source, so the
|
|
170
|
+
* doublecheck report can fold the record without re-parsing the prose.
|
|
171
|
+
* @param outcome - the settled review outcome.
|
|
172
|
+
* @returns the injection-ready user message.
|
|
173
|
+
*/
|
|
174
|
+
export function reviewInjection(outcome) {
|
|
175
|
+
const source = {
|
|
176
|
+
kind: 'doublecheck-review',
|
|
177
|
+
verdict: outcome.verdict,
|
|
178
|
+
findings: outcome.findings,
|
|
179
|
+
};
|
|
180
|
+
return createUserMessage({
|
|
181
|
+
content: [{ type: 'text', text: outcome.text }],
|
|
182
|
+
source,
|
|
183
|
+
});
|
|
184
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-doublecheck package entry: the shared domain model and pure helpers.
|
|
3
|
+
* The installable plugin rows live at the `./grill` and `./guard` subpath
|
|
4
|
+
* exports (see cordis.patch.yml); this root exists for tools, tests, and
|
|
5
|
+
* future modules that need the package's private domain vocabulary.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-doublecheck
|
|
8
|
+
*/
|
|
9
|
+
export type { GrilledSpec, GuardIntensity, GuardGate, GuardVerdict } from './events.ts';
|
|
10
|
+
export * from './domain/stages.ts';
|
|
11
|
+
export * from './domain/evidence.ts';
|
|
12
|
+
export * from './domain/vagueness.ts';
|
|
13
|
+
export * from './domain/vocabulary.ts';
|
|
14
|
+
export * from './domain/report.ts';
|
|
15
|
+
export { renderSpecMarkdown } from './grill/index.ts';
|
|
16
|
+
export { BundledSkillProvider, parseSkillAsset, PROVIDER_NAME } from './grill/provider.ts';
|
|
17
|
+
export type * from './types.ts';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-doublecheck package entry: the shared domain model and pure helpers.
|
|
3
|
+
* The installable plugin rows live at the `./grill` and `./guard` subpath
|
|
4
|
+
* exports (see cordis.patch.yml); this root exists for tools, tests, and
|
|
5
|
+
* future modules that need the package's private domain vocabulary.
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-doublecheck
|
|
8
|
+
*/
|
|
9
|
+
export * from "./domain/stages.js";
|
|
10
|
+
export * from "./domain/evidence.js";
|
|
11
|
+
export * from "./domain/vagueness.js";
|
|
12
|
+
export * from "./domain/vocabulary.js";
|
|
13
|
+
export * from "./domain/report.js";
|
|
14
|
+
export { renderSpecMarkdown } from "./grill/index.js";
|
|
15
|
+
export { BundledSkillProvider, parseSkillAsset, PROVIDER_NAME } from "./grill/provider.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for dsh-doublecheck.
|
|
3
|
+
*
|
|
4
|
+
* Three post-commit diagnostic checks over the events this package's write
|
|
5
|
+
* paths produce (all three target events are committed before dispatch, so a
|
|
6
|
+
* violation is reported — via the host registry's `fail` — rather than
|
|
7
|
+
* vetoed):
|
|
8
|
+
*
|
|
9
|
+
* 1. Every `doublecheck/spec` announcement carries six non-empty spec fields.
|
|
10
|
+
* 2. Every `doublecheck/report` announcement's verdict is consistent with the
|
|
11
|
+
* log re-derivation. When verification did not run, the two must be equal;
|
|
12
|
+
* when verification ran, a failing check must report `challenged` and a
|
|
13
|
+
* clean one must report `proven` or `unverified` (the `complete` fact rides
|
|
14
|
+
* the tool result, not this event, so equality is not checkable there).
|
|
15
|
+
* 3. Every `doublecheck/review` announcement's findings satisfy the
|
|
16
|
+
* structured finding shape (severity enum, non-empty title and detail).
|
|
17
|
+
*
|
|
18
|
+
* The guard registers a facts-bearing installer from its own context; the
|
|
19
|
+
* `./invariant` export is the standalone companion usable through a separate
|
|
20
|
+
* profile row.
|
|
21
|
+
* @module dsh-doublecheck/invariant
|
|
22
|
+
*/
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import type { TestRunDetection } from './domain/evidence.ts';
|
|
25
|
+
/** Full npm package name owning the reported failures. */
|
|
26
|
+
export declare const PACKAGE_NAME = "dsh-doublecheck";
|
|
27
|
+
/** A package-attributed invariant failure reported by the host registry. */
|
|
28
|
+
export type InvariantFailure = (message: string) => never;
|
|
29
|
+
/** Facts the installer needs beyond the event stream. */
|
|
30
|
+
export interface InvariantFacts {
|
|
31
|
+
/** The compiled red/green detection backing the report re-derivation. */
|
|
32
|
+
detection(): TestRunDetection;
|
|
33
|
+
}
|
|
34
|
+
/** Installer callback accepted by the host's invariant registry. */
|
|
35
|
+
export type InvariantInstaller = (ctx: Context, fail: InvariantFailure) => void | Promise<void>;
|
|
36
|
+
/** Minimal runtime contract used by the companion without a source checkout. */
|
|
37
|
+
export interface InvariantRegistry {
|
|
38
|
+
register(packageName: string, installer: InvariantInstaller): () => void;
|
|
39
|
+
}
|
|
40
|
+
/** Cordis companion plugin name. */
|
|
41
|
+
export declare const name = "dsh-doublecheck-invariant";
|
|
42
|
+
/** Service required before the companion can reserve package ownership. */
|
|
43
|
+
export declare const inject: string[];
|
|
44
|
+
/**
|
|
45
|
+
* Build the installer over a facts source. The standalone companion and the
|
|
46
|
+
* main plugin share this body; only the facts differ.
|
|
47
|
+
* @param facts - the report re-derivation knobs.
|
|
48
|
+
* @returns the installer the host registry activates in its child context.
|
|
49
|
+
*/
|
|
50
|
+
export declare function installInvariant(facts: InvariantFacts): InvariantInstaller;
|
|
51
|
+
/**
|
|
52
|
+
* Register the standalone companion with envelope-only facts.
|
|
53
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
54
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
55
|
+
*/
|
|
56
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for dsh-doublecheck.
|
|
3
|
+
*
|
|
4
|
+
* Three post-commit diagnostic checks over the events this package's write
|
|
5
|
+
* paths produce (all three target events are committed before dispatch, so a
|
|
6
|
+
* violation is reported — via the host registry's `fail` — rather than
|
|
7
|
+
* vetoed):
|
|
8
|
+
*
|
|
9
|
+
* 1. Every `doublecheck/spec` announcement carries six non-empty spec fields.
|
|
10
|
+
* 2. Every `doublecheck/report` announcement's verdict is consistent with the
|
|
11
|
+
* log re-derivation. When verification did not run, the two must be equal;
|
|
12
|
+
* when verification ran, a failing check must report `challenged` and a
|
|
13
|
+
* clean one must report `proven` or `unverified` (the `complete` fact rides
|
|
14
|
+
* the tool result, not this event, so equality is not checkable there).
|
|
15
|
+
* 3. Every `doublecheck/review` announcement's findings satisfy the
|
|
16
|
+
* structured finding shape (severity enum, non-empty title and detail).
|
|
17
|
+
*
|
|
18
|
+
* The guard registers a facts-bearing installer from its own context; the
|
|
19
|
+
* `./invariant` export is the standalone companion usable through a separate
|
|
20
|
+
* profile row.
|
|
21
|
+
* @module dsh-doublecheck/invariant
|
|
22
|
+
*/
|
|
23
|
+
import { deriveReportVerdict, foldReportFacts } from "./domain/report.js";
|
|
24
|
+
/** Full npm package name owning the reported failures. */
|
|
25
|
+
export const PACKAGE_NAME = 'dsh-doublecheck';
|
|
26
|
+
/** Cordis companion plugin name. */
|
|
27
|
+
export const name = 'dsh-doublecheck-invariant';
|
|
28
|
+
/** Service required before the companion can reserve package ownership. */
|
|
29
|
+
export const inject = ['invariants'];
|
|
30
|
+
/** All six spec fields, in declaration order. */
|
|
31
|
+
const SPEC_FIELDS = [
|
|
32
|
+
'goal', 'scope', 'acceptanceCriteria', 'failureModes', 'priorities', 'nonGoals',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Build the installer over a facts source. The standalone companion and the
|
|
36
|
+
* main plugin share this body; only the facts differ.
|
|
37
|
+
* @param facts - the report re-derivation knobs.
|
|
38
|
+
* @returns the installer the host registry activates in its child context.
|
|
39
|
+
*/
|
|
40
|
+
export function installInvariant(facts) {
|
|
41
|
+
return (ctx, fail) => {
|
|
42
|
+
ctx.on('doublecheck/spec', (payload) => {
|
|
43
|
+
for (const field of SPEC_FIELDS) {
|
|
44
|
+
if (typeof payload.spec[field] !== 'string' || payload.spec[field].trim() === '') {
|
|
45
|
+
fail(`doublecheck/spec announcement carries an empty "${field}" field`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
ctx.on('doublecheck/report', (payload) => {
|
|
50
|
+
const folded = foldReportFacts(payload.session.events, facts.detection());
|
|
51
|
+
if (payload.verification === null) {
|
|
52
|
+
const rederived = deriveReportVerdict(folded, null);
|
|
53
|
+
if (rederived !== payload.verdict) {
|
|
54
|
+
fail(`doublecheck/report announces verdict "${payload.verdict}" but the log re-derives "${rederived}"`);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const hasFailure = payload.verification.checks.some(check => check.verdict === 'fail');
|
|
59
|
+
if (hasFailure && payload.verdict !== 'challenged') {
|
|
60
|
+
fail(`doublecheck/report carries a failing verification check but announces verdict "${payload.verdict}"`);
|
|
61
|
+
}
|
|
62
|
+
if (!hasFailure && payload.verdict !== 'proven' && payload.verdict !== 'unverified') {
|
|
63
|
+
fail(`doublecheck/report carries clean verification checks but announces verdict "${payload.verdict}"`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
ctx.on('doublecheck/review', (payload) => {
|
|
67
|
+
const verdicts = ['findings', 'clean', 'unavailable'];
|
|
68
|
+
if (!verdicts.includes(payload.verdict)) {
|
|
69
|
+
fail(`doublecheck/review announces invalid verdict ${JSON.stringify(payload.verdict)}`);
|
|
70
|
+
}
|
|
71
|
+
for (const finding of payload.findings) {
|
|
72
|
+
const severities = ['blocker', 'major', 'minor', 'info'];
|
|
73
|
+
if (!severities.includes(finding.severity)) {
|
|
74
|
+
fail(`doublecheck/review finding has invalid severity ${JSON.stringify(finding.severity)}`);
|
|
75
|
+
}
|
|
76
|
+
if (typeof finding.title !== 'string' || finding.title.trim() === '') {
|
|
77
|
+
fail('doublecheck/review finding has an empty title');
|
|
78
|
+
}
|
|
79
|
+
if (typeof finding.detail !== 'string' || finding.detail.trim() === '') {
|
|
80
|
+
fail('doublecheck/review finding has an empty detail');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the host registry through Cordis's named service lookup. Keeping
|
|
88
|
+
* this narrow local contract lets the companion build without host source
|
|
89
|
+
* files; a composed DSH profile still supplies the real `invariants` service.
|
|
90
|
+
* @param ctx - Cordis context carrying the host service.
|
|
91
|
+
* @returns the host invariant registry.
|
|
92
|
+
* @throws {Error} when the companion is loaded without its host service.
|
|
93
|
+
*/
|
|
94
|
+
function getInvariantRegistry(ctx) {
|
|
95
|
+
const registry = ctx.get('invariants');
|
|
96
|
+
if (registry === undefined) {
|
|
97
|
+
throw new Error(`invariant companion requires the "invariants" service for ${PACKAGE_NAME}`);
|
|
98
|
+
}
|
|
99
|
+
return registry;
|
|
100
|
+
}
|
|
101
|
+
/** Facts for the standalone companion: the report check degrades to no test-run evidence. */
|
|
102
|
+
const COMPANION_FACTS = {
|
|
103
|
+
detection: () => ({
|
|
104
|
+
testToolNames: [], testCommandPatterns: [], mutationTools: [], testFilePatterns: [],
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Register the standalone companion with envelope-only facts.
|
|
109
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
110
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
111
|
+
*/
|
|
112
|
+
export const apply = (ctx) => Promise.resolve(getInvariantRegistry(ctx).register(PACKAGE_NAME, installInvariant(COMPANION_FACTS)));
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable type-table merges owned by this package.
|
|
3
|
+
*
|
|
4
|
+
* The `doublecheck` projection key is declared here (its one home) and
|
|
5
|
+
* re-exported from the package root so consumers receive the
|
|
6
|
+
* `SessionProjectionMap` merge.
|
|
7
|
+
* @module dsh-doublecheck/types
|
|
8
|
+
*/
|
|
9
|
+
import { z as zod } from 'zod';
|
|
10
|
+
import type { DisciplineStage, TestColor } from './domain/stages.ts';
|
|
11
|
+
/** The whole wire value of the `doublecheck` session projection. */
|
|
12
|
+
export interface DoublecheckView {
|
|
13
|
+
/** The last discipline stage reached on durable evidence. */
|
|
14
|
+
stage: DisciplineStage;
|
|
15
|
+
/** The latest test-run evidence color. */
|
|
16
|
+
color: TestColor;
|
|
17
|
+
/** Whether a committed requirements spec exists in the log. */
|
|
18
|
+
hasSpec: boolean;
|
|
19
|
+
/** The goal of the latest committed spec, or '' before any spec. */
|
|
20
|
+
specGoal: string;
|
|
21
|
+
/** Whether an adversary review record exists in the log. */
|
|
22
|
+
reviewed: boolean;
|
|
23
|
+
/** Total implementation edits folded so far. */
|
|
24
|
+
editCount: number;
|
|
25
|
+
}
|
|
26
|
+
/** Validates the `doublecheck` projection's wire payload before it leaves the host. */
|
|
27
|
+
export declare const doublecheckViewSchema: zod.ZodObject<{
|
|
28
|
+
stage: zod.ZodUnion<readonly [zod.ZodLiteral<"grill">, zod.ZodLiteral<"design">, zod.ZodLiteral<"red">, zod.ZodLiteral<"green">, zod.ZodLiteral<"review">, zod.ZodLiteral<"verify">]>;
|
|
29
|
+
color: zod.ZodUnion<readonly [zod.ZodLiteral<"none">, zod.ZodLiteral<"red">, zod.ZodLiteral<"green">]>;
|
|
30
|
+
hasSpec: zod.ZodBoolean;
|
|
31
|
+
specGoal: zod.ZodString;
|
|
32
|
+
reviewed: zod.ZodBoolean;
|
|
33
|
+
editCount: zod.ZodNumber;
|
|
34
|
+
}, zod.core.$strip>;
|
|
35
|
+
declare module '@deepseek-ai/dsh-session-projection/types' {
|
|
36
|
+
interface SessionProjectionMap {
|
|
37
|
+
doublecheck: DoublecheckView;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable type-table merges owned by this package.
|
|
3
|
+
*
|
|
4
|
+
* The `doublecheck` projection key is declared here (its one home) and
|
|
5
|
+
* re-exported from the package root so consumers receive the
|
|
6
|
+
* `SessionProjectionMap` merge.
|
|
7
|
+
* @module dsh-doublecheck/types
|
|
8
|
+
*/
|
|
9
|
+
import { z as zod } from 'zod';
|
|
10
|
+
/** Validates the `doublecheck` projection's wire payload before it leaves the host. */
|
|
11
|
+
export const doublecheckViewSchema = zod.object({
|
|
12
|
+
stage: zod.union([zod.literal('grill'), zod.literal('design'), zod.literal('red'), zod.literal('green'), zod.literal('review'), zod.literal('verify')]),
|
|
13
|
+
color: zod.union([zod.literal('none'), zod.literal('red'), zod.literal('green')]),
|
|
14
|
+
hasSpec: zod.boolean(),
|
|
15
|
+
specGoal: zod.string(),
|
|
16
|
+
reviewed: zod.boolean(),
|
|
17
|
+
editCount: zod.number(),
|
|
18
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-doublecheck",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Double-check before you ship: grill the requirements, test the implementation, prove the delivery.",
|
|
5
|
+
"packageManager": "pnpm@11.7.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/PerryLink/dsh-doublecheck.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/PerryLink/dsh-doublecheck#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/PerryLink/dsh-doublecheck/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"types": "lib/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./lib/index.d.ts",
|
|
20
|
+
"import": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./grill": {
|
|
23
|
+
"types": "./lib/grill/index.d.ts",
|
|
24
|
+
"import": "./lib/grill/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./guard": {
|
|
27
|
+
"types": "./lib/guard/index.d.ts",
|
|
28
|
+
"import": "./lib/guard/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"lib",
|
|
34
|
+
"skills",
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"README.md",
|
|
38
|
+
"README.zh.md",
|
|
39
|
+
"README.es.md",
|
|
40
|
+
"README.pt.md",
|
|
41
|
+
"README.hi.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc --noEmitOnError",
|
|
46
|
+
"prepare": "tsc --noEmitOnError",
|
|
47
|
+
"prepublishOnly": "tsc --noEmitOnError && pnpm run test",
|
|
48
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.tests.json",
|
|
49
|
+
"lint": "eslint src tests",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"pack:check": "pnpm run build && pnpm pack"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"dsh",
|
|
56
|
+
"dsh-plugin",
|
|
57
|
+
"deepseek-harness",
|
|
58
|
+
"engineering-discipline",
|
|
59
|
+
"requirements",
|
|
60
|
+
"guard",
|
|
61
|
+
"skill"
|
|
62
|
+
],
|
|
63
|
+
"author": "PerryLink",
|
|
64
|
+
"license": "Apache-2.0",
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
67
|
+
},
|
|
68
|
+
"dsh": {
|
|
69
|
+
"bundle": {
|
|
70
|
+
"patch": "./cordis.patch.yml"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
75
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
76
|
+
"@deepseek-ai/dsh-commands": "0.1.0-rc.6",
|
|
77
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
78
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
79
|
+
"@deepseek-ai/dsh-session-projection": "0.1.0-rc.6",
|
|
80
|
+
"@deepseek-ai/dsh-skill": "0.1.0-rc.6",
|
|
81
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
82
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
83
|
+
},
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"typescript": "^5.7.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
89
|
+
"@deepseek-ai/dsh-agent": "0.1.0-rc.6",
|
|
90
|
+
"@deepseek-ai/dsh-commands": "0.1.0-rc.6",
|
|
91
|
+
"@deepseek-ai/dsh-fs": "0.1.0-rc.6",
|
|
92
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
93
|
+
"@deepseek-ai/dsh-session": "0.1.0-rc.6",
|
|
94
|
+
"@deepseek-ai/dsh-session-projection": "0.1.0-rc.6",
|
|
95
|
+
"@deepseek-ai/dsh-skill": "0.1.0-rc.6",
|
|
96
|
+
"@deepseek-ai/dsh-subagent": "0.1.0-rc.6",
|
|
97
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.6",
|
|
98
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6",
|
|
99
|
+
"@deepseek-ai/dsh-workflow": "0.1.0-rc.6",
|
|
100
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
101
|
+
"@eslint/js": "^9.30.0",
|
|
102
|
+
"@types/node": "^22.15.0",
|
|
103
|
+
"@vitest/coverage-v8": "^3.2.0",
|
|
104
|
+
"eslint": "^9.30.0",
|
|
105
|
+
"typescript-eslint": "^8.35.0",
|
|
106
|
+
"vitest": "^3.2.0",
|
|
107
|
+
"zod": "^4.4.3"
|
|
108
|
+
}
|
|
109
|
+
}
|