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.
Files changed (45) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +252 -0
  4. package/README.hi.md +252 -0
  5. package/README.md +268 -0
  6. package/README.pt.md +252 -0
  7. package/README.zh.md +266 -0
  8. package/cordis.patch.yml +19 -0
  9. package/lib/domain/evidence.d.ts +100 -0
  10. package/lib/domain/evidence.js +180 -0
  11. package/lib/domain/projection.d.ts +60 -0
  12. package/lib/domain/projection.js +132 -0
  13. package/lib/domain/report.d.ts +93 -0
  14. package/lib/domain/report.js +266 -0
  15. package/lib/domain/stages.d.ts +98 -0
  16. package/lib/domain/stages.js +192 -0
  17. package/lib/domain/vagueness.d.ts +25 -0
  18. package/lib/domain/vagueness.js +56 -0
  19. package/lib/domain/vocabulary.d.ts +50 -0
  20. package/lib/domain/vocabulary.js +16 -0
  21. package/lib/events.d.ts +155 -0
  22. package/lib/events.js +15 -0
  23. package/lib/grill/index.d.ts +49 -0
  24. package/lib/grill/index.js +495 -0
  25. package/lib/grill/provider.d.ts +56 -0
  26. package/lib/grill/provider.js +189 -0
  27. package/lib/guard/command.d.ts +47 -0
  28. package/lib/guard/command.js +132 -0
  29. package/lib/guard/index.d.ts +111 -0
  30. package/lib/guard/index.js +516 -0
  31. package/lib/guard/prose.d.ts +34 -0
  32. package/lib/guard/prose.js +80 -0
  33. package/lib/guard/review.d.ts +67 -0
  34. package/lib/guard/review.js +184 -0
  35. package/lib/index.d.ts +17 -0
  36. package/lib/index.js +15 -0
  37. package/lib/invariant.d.ts +56 -0
  38. package/lib/invariant.js +112 -0
  39. package/lib/types.d.ts +39 -0
  40. package/lib/types.js +18 -0
  41. package/package.json +109 -0
  42. package/skills/delivery-proof/SKILL.md +52 -0
  43. package/skills/delivery-review/SKILL.md +49 -0
  44. package/skills/grill-requirements/SKILL.md +98 -0
  45. package/skills/red-green-tdd/SKILL.md +57 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * The doublecheck report fold: consolidate the durable discipline evidence of
3
+ * one session into report facts, derive the delivery verdict, and render the
4
+ * model-facing report document.
5
+ *
6
+ * Everything here folds the session log alone — the spec (from the
7
+ * `doublecheck_spec` call arguments), the test-run timeline (red/green
8
+ * outcomes), the implementation edits, and the injected
9
+ * `doublecheck-review` source records — so a report re-derived after resume
10
+ * or fork reads identically.
11
+ *
12
+ * @module dsh-doublecheck/domain/report
13
+ */
14
+ import { isTestCommand, isTestFilePath, joinTextBlocks, mutationTargetPath, parseRawArguments, shellCommand, testOutcome, } from "./evidence.js";
15
+ import { SPEC_TOOL_NAME } from "./stages.js";
16
+ import { VERIFY_DIMENSIONS, } from "./vocabulary.js";
17
+ /** Maximum timeline rows rendered into the report document. */
18
+ const TIMELINE_RENDER_CAP = 20;
19
+ /** Empty facts: nothing evidenced yet. */
20
+ export function emptyReportFacts() {
21
+ return { spec: null, testRuns: { failed: 0, passed: 0 }, timeline: [], edits: 0, review: null };
22
+ }
23
+ /**
24
+ * Fold a session log into report facts.
25
+ * @param events - the session's append-only event log.
26
+ * @param detection - the compiled test-run/mutation detection knobs.
27
+ * @returns the complete folded facts.
28
+ */
29
+ export function foldReportFacts(events, detection) {
30
+ const facts = emptyReportFacts();
31
+ const pendingTests = new Map();
32
+ /** Spec calls awaiting a result: only a successful pair commits the spec. */
33
+ const pendingSpecs = new Map();
34
+ for (const event of events) {
35
+ switch (event.type) {
36
+ case 'tool/call': {
37
+ const args = parseRawArguments(event.data.arguments);
38
+ if (event.data.name === SPEC_TOOL_NAME) {
39
+ const spec = readSpecFromArgs(args);
40
+ if (spec !== null)
41
+ pendingSpecs.set(event.data.callId, spec);
42
+ }
43
+ const command = shellCommand(event.data.name, args, detection);
44
+ if (command !== undefined && isTestCommand(command, detection)) {
45
+ pendingTests.set(event.data.callId, command);
46
+ }
47
+ const path = mutationTargetPath(event.data.name, args, detection);
48
+ if (path !== undefined && !isTestFilePath(path, detection)) {
49
+ facts.edits += 1;
50
+ }
51
+ break;
52
+ }
53
+ case 'tool/result': {
54
+ const callId = event.data.message.source.callId;
55
+ const spec = pendingSpecs.get(callId);
56
+ if (spec !== undefined && event.data.error === undefined) {
57
+ pendingSpecs.delete(callId);
58
+ facts.spec = spec;
59
+ facts.timeline.push({ kind: 'spec', detail: preview(spec.goal) });
60
+ }
61
+ else if (spec !== undefined) {
62
+ pendingSpecs.delete(callId);
63
+ }
64
+ const command = pendingTests.get(callId);
65
+ if (command === undefined)
66
+ break;
67
+ pendingTests.delete(callId);
68
+ foldTestRun(facts, command, testOutcome(joinTextBlocks(event.data.message.content), event.data.error !== undefined));
69
+ break;
70
+ }
71
+ case 'tool/code-dispatch': {
72
+ const args = parseRawArguments(event.data.arguments);
73
+ const command = shellCommand(event.data.name, args, detection);
74
+ if (command !== undefined && isTestCommand(command, detection)) {
75
+ foldTestRun(facts, command, testOutcome(joinTextBlocks(event.data.content), event.data.isError));
76
+ }
77
+ // Code Mode dispatches run through the same pre-execute policy gates,
78
+ // so a dispatched edit is a real implementation edit and must count
79
+ // toward the report exactly like a native call.
80
+ const path = mutationTargetPath(event.data.name, args, detection);
81
+ if (path !== undefined && !isTestFilePath(path, detection)) {
82
+ facts.edits += 1;
83
+ }
84
+ break;
85
+ }
86
+ case 'user/message': {
87
+ const source = event.data.source;
88
+ if (source.kind !== 'doublecheck-review')
89
+ break;
90
+ const record = source;
91
+ facts.review = { verdict: record.verdict, findings: [...record.findings] };
92
+ facts.timeline.push({ kind: 'review', detail: record.verdict });
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ return facts;
98
+ }
99
+ /** Fold one settled test-run outcome into the counts and timeline. */
100
+ function foldTestRun(facts, command, outcome) {
101
+ if (outcome === 'fail') {
102
+ facts.testRuns.failed += 1;
103
+ facts.timeline.push({ kind: 'red', detail: preview(command) });
104
+ }
105
+ else if (outcome === 'pass') {
106
+ facts.testRuns.passed += 1;
107
+ facts.timeline.push({ kind: 'green', detail: preview(command) });
108
+ }
109
+ }
110
+ /** Read the six spec fields from a spec tool call's arguments, or null when malformed. */
111
+ function readSpecFromArgs(args) {
112
+ if (args === undefined)
113
+ return null;
114
+ const { goal, scope, acceptanceCriteria, failureModes, priorities, nonGoals } = args;
115
+ if (typeof goal !== 'string' || typeof scope !== 'string' || typeof acceptanceCriteria !== 'string'
116
+ || typeof failureModes !== 'string' || typeof priorities !== 'string' || typeof nonGoals !== 'string') {
117
+ return null;
118
+ }
119
+ return { goal, scope, acceptanceCriteria, failureModes, priorities, nonGoals };
120
+ }
121
+ /**
122
+ * Derive the delivery verdict from the folded facts, the optional review,
123
+ * and the optional verification outcome.
124
+ * @param facts - the folded session facts.
125
+ * @param verification - verification outcome when the verify workflow ran, else null.
126
+ * @returns the report verdict.
127
+ */
128
+ export function deriveReportVerdict(facts, verification) {
129
+ if (facts.spec === null)
130
+ return 'grill';
131
+ if (facts.edits === 0)
132
+ return 'draft';
133
+ const lastTest = facts.timeline.findLast(entry => entry.kind === 'red' || entry.kind === 'green');
134
+ if (lastTest?.kind === 'red')
135
+ return 'red';
136
+ if (verification !== null) {
137
+ if (verification.checks.some(check => check.verdict === 'fail'))
138
+ return 'challenged';
139
+ // A missing dimension is not a pass: `proven` requires a verdict for
140
+ // every spec dimension.
141
+ return verification.complete ? 'proven' : 'unverified';
142
+ }
143
+ if (facts.review === null || facts.review.verdict === 'unavailable')
144
+ return 'green';
145
+ return facts.review.verdict === 'findings' ? 'objections' : 'verified';
146
+ }
147
+ /** Render the report facts as the model-facing markdown document. */
148
+ export function renderReportMarkdown(data) {
149
+ const lines = [
150
+ '# Doublecheck report',
151
+ '',
152
+ `> Verdict: **${data.verdict}**`,
153
+ '',
154
+ '## Spec',
155
+ ];
156
+ if (data.spec === null) {
157
+ lines.push('No spec recorded; the requirements grill has not committed anything yet.', '');
158
+ }
159
+ else {
160
+ lines.push(`- Goal: ${data.spec.goal}`, `- Scope: ${data.spec.scope}`, `- Acceptance criteria: ${data.spec.acceptanceCriteria}`, `- Failure modes: ${data.spec.failureModes}`, `- Priorities: ${data.spec.priorities}`, `- Non-goals: ${data.spec.nonGoals}`, '');
161
+ }
162
+ lines.push('## Test evidence', `- failing runs: ${data.testRuns.failed}`, `- passing runs: ${data.testRuns.passed}`, '');
163
+ const rows = data.timeline.slice(-TIMELINE_RENDER_CAP);
164
+ lines.push(...rows.map(row => `- [${row.kind}] ${row.detail}`));
165
+ if (data.timeline.length > rows.length) {
166
+ lines.push(`- … ${data.timeline.length - rows.length} earlier rows omitted`);
167
+ }
168
+ lines.push('', '## Adversary review');
169
+ if (data.review === null) {
170
+ lines.push('No adversary review ran for this session.');
171
+ }
172
+ else if (data.review.verdict === 'findings') {
173
+ lines.push(...data.review.findings.map(finding => `- [${finding.severity}] ${finding.title} — ${finding.detail}`));
174
+ }
175
+ else {
176
+ lines.push(`Verdict: ${data.review.verdict}`);
177
+ }
178
+ lines.push('', '## Verification');
179
+ if (data.verification === null) {
180
+ lines.push('Not run.');
181
+ }
182
+ else {
183
+ for (const check of data.verification.checks) {
184
+ lines.push(`- [${check.verdict}] ${check.dimension}: ${check.evidence} ${check.note.length > 0 ? `(${check.note})` : ''}`.trim());
185
+ }
186
+ if (!data.verification.complete) {
187
+ lines.push(`- … not every spec dimension returned a verdict; the delivery is not proven.`);
188
+ }
189
+ }
190
+ lines.push('', `## Delivery`, `- implementation edits: ${data.edits}`, '');
191
+ return lines.join('\n');
192
+ }
193
+ /** The structured output each verification child must satisfy. */
194
+ export const VERIFY_CHECK_SCHEMA = {
195
+ type: 'object',
196
+ additionalProperties: false,
197
+ required: ['dimension', 'verdict', 'evidence', 'note'],
198
+ properties: {
199
+ dimension: { type: 'string', enum: [...VERIFY_DIMENSIONS] },
200
+ verdict: { type: 'string', enum: ['pass', 'fail'] },
201
+ evidence: { type: 'string' },
202
+ note: { type: 'string' },
203
+ },
204
+ };
205
+ /** The verify workflow identity block. */
206
+ export const VERIFY_META = {
207
+ name: 'doublecheck-verify',
208
+ description: 'Verify a doublecheck delivery across the six spec dimensions and return one check per dimension.',
209
+ phases: [{ title: 'doublecheck verify' }],
210
+ };
211
+ /**
212
+ * The single-checker structured output: one agent auditing every spec
213
+ * dimension and returning a check per dimension. Mirrors the parallel
214
+ * mode's `{ checks: [...] }` result shape so the fold is identical.
215
+ */
216
+ const VERIFY_BATCH_SCHEMA = {
217
+ type: 'object',
218
+ additionalProperties: false,
219
+ required: ['checks'],
220
+ properties: {
221
+ checks: { type: 'array', items: VERIFY_CHECK_SCHEMA },
222
+ },
223
+ };
224
+ /**
225
+ * Build the verify workflow script body.
226
+ * @param mode - `all` fans out one parallel checker per spec dimension;
227
+ * `single` runs one checker over every dimension (cheaper, one subagent).
228
+ * @returns the plain-JS script body for `WorkflowStartRequest.script`.
229
+ */
230
+ export function buildVerifyScript(mode) {
231
+ if (mode === 'single') {
232
+ return [
233
+ "phase('doublecheck verify')",
234
+ 'const checks = await agent(',
235
+ " 'You verify every dimension of a requirements spec against the session you inherited. "
236
+ + "The spec: ' + JSON.stringify(args.spec) + '. "
237
+ + 'Assume the delivery FAILS each dimension. Examine the inherited session for evidence: '
238
+ + 'the recorded spec, the red/green test runs, the edits, and any review findings. '
239
+ + 'Answer through the required structured output: one check per dimension; pass only when the session evidence '
240
+ + 'satisfies that dimension, fail otherwise, with evidence cited from the session '
241
+ + "and a note on what to fix.',",
242
+ ' { label: \'verify-all\', schema: ' + JSON.stringify(VERIFY_BATCH_SCHEMA) + ' })',
243
+ 'return { checks: checks.checks.filter(Boolean) }',
244
+ ].join('\n');
245
+ }
246
+ return [
247
+ "phase('doublecheck verify')",
248
+ 'const checks = await parallel(args.dimensions.map(function (dimension) {',
249
+ ' return function () {',
250
+ " return agent('You verify one dimension of a requirements spec against the session you inherited. "
251
+ + "The dimension: ' + dimension + '. The spec says: ' + args.spec[dimension] + '. "
252
+ + "Assume the delivery FAILS this dimension. Examine the inherited session for evidence: "
253
+ + "the recorded spec, the red/green test runs, the edits, and any review findings. "
254
+ + "Answer through the required structured output: pass only when the session evidence "
255
+ + "satisfies the dimension; fail otherwise, with evidence cited from the session "
256
+ + "and a note on what to fix.',",
257
+ ` { label: 'verify-' + dimension, schema: ${JSON.stringify(VERIFY_CHECK_SCHEMA)} })`,
258
+ ' }',
259
+ '}))',
260
+ 'return { checks: checks.filter(Boolean) }',
261
+ ].join('\n');
262
+ }
263
+ /** Truncate a spec goal for timeline rows. */
264
+ function preview(text) {
265
+ return text.length > 120 ? `${text.slice(0, 120)}…` : text;
266
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * The dsh-doublecheck discipline state machine.
3
+ *
4
+ * Six stages: `grill` (settle requirements) → `design` (spec committed) →
5
+ * `red` (failing test) → `green` (passing test) → `review` (self-review) →
6
+ * `verify` (delivery proof). v0.1 implemented the first transition; v0.2 adds
7
+ * the red/green evidence transitions folded from durable test-run records.
8
+ *
9
+ * The state derives from the session log, never from process memory: a
10
+ * `doublecheck_spec` call with a successful result advances past `grill`, a
11
+ * failing test run marks `red`, a passing run marks `green`, and any
12
+ * implementation edit after the latest passing run re-arms the green gate.
13
+ * Resumed and forked sessions fold to the same state as the live run.
14
+ *
15
+ * @module dsh-doublecheck/domain/stages
16
+ */
17
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
18
+ import { type TestRunDetection } from './evidence.ts';
19
+ /** The discipline stages, in execution order. */
20
+ export type DisciplineStage = 'grill' | 'design' | 'red' | 'green' | 'review' | 'verify';
21
+ /** All discipline stages in order. */
22
+ export declare const DISCIPLINE_STAGES: readonly DisciplineStage[];
23
+ /** Name of the model-facing tool that commits a grilled requirements spec. */
24
+ export declare const SPEC_TOOL_NAME = "doublecheck_spec";
25
+ /** Name of the model-facing tool that consolidates the delivery report. */
26
+ export declare const REPORT_TOOL_NAME = "doublecheck_report";
27
+ /** The current red/green color of the session: the latest test-run evidence. */
28
+ export type TestColor = 'none' | 'red' | 'green';
29
+ /**
30
+ * The folded discipline facts for one session log. The `pending*` sets are
31
+ * fold bookkeeping: calls whose results have not been folded yet, carried
32
+ * across incremental fold batches.
33
+ */
34
+ export interface DisciplineState {
35
+ /** The last stage a successful discipline call or test run reached. */
36
+ stage: DisciplineStage;
37
+ /** Whether a successful `doublecheck_spec` call exists in the log. */
38
+ hasSpec: boolean;
39
+ /** The log sequence of the latest successful spec commit (0 = none). Guards compare it with the latest direct user task to reopen the grill on new tasks. */
40
+ specSeq: number;
41
+ /** The latest test-run evidence: red = failing since the last pass. */
42
+ color: TestColor;
43
+ /** An implementation edit happened after the latest passing test run. */
44
+ pendingGreen: boolean;
45
+ /** Total implementation edits folded so far (the green-gate reminder epoch). */
46
+ editCount: number;
47
+ /** Stage-tool call ids whose results have not been folded yet, mapped to the stage they advance. */
48
+ pendingStageCalls: Map<string, DisciplineStage>;
49
+ /** Test-run call ids whose results have not been folded yet. */
50
+ pendingTestCalls: Set<string>;
51
+ }
52
+ /** A fresh fold state: nothing evidenced yet. */
53
+ export declare function emptyDisciplineState(): DisciplineState;
54
+ /**
55
+ * Call ids of successful invocations of `toolName` in the given event log. A
56
+ * call is successful when some `tool/result` pairs with its `callId` and
57
+ * carries no error record. One pass over the log: results always follow
58
+ * their calls, so a pending call set pairs them in place.
59
+ * @param events - the session's append-only event log.
60
+ * @param toolName - the tool whose successful calls are collected.
61
+ * @returns every successful call id, in log order.
62
+ */
63
+ export declare function successfulToolCalls(events: readonly SessionEvent[], toolName: string): string[];
64
+ /**
65
+ * Fold `events[start..end)` into the discipline state. Each event is folded
66
+ * once; the state's pending sets carry unmatched calls across batches, so the
67
+ * caller can advance an existing state over the newly appended tail instead
68
+ * of refolding the whole log.
69
+ * @param state - the state to advance (start one via {@link emptyDisciplineState}).
70
+ * @param events - the session's append-only event log.
71
+ * @param start - first index to fold.
72
+ * @param detection - the compiled red/green evidence knobs.
73
+ * @returns the same state instance, advanced.
74
+ */
75
+ export declare function foldDisciplineRange(state: DisciplineState, events: readonly SessionEvent[], start: number, detection: TestRunDetection): DisciplineState;
76
+ /**
77
+ * Fold a session log from scratch to its current discipline state.
78
+ * @param events - the session's append-only event log.
79
+ * @param detection - the compiled red/green evidence knobs; omit to ignore test runs.
80
+ * @returns the complete folded state.
81
+ */
82
+ export declare function foldDisciplineState(events: readonly SessionEvent[], detection?: TestRunDetection): DisciplineState;
83
+ /**
84
+ * Fold a session log to its current discipline stage. Only successful calls
85
+ * and real test runs advance the stage; a failed spec attempt leaves the
86
+ * session in `grill`, and red/green move with the latest test evidence.
87
+ * @param events - the session's append-only event log.
88
+ * @param detection - the compiled red/green evidence knobs; omit to ignore test runs.
89
+ * @returns the last stage reached, or `grill`.
90
+ */
91
+ export declare function foldDisciplineStage(events: readonly SessionEvent[], detection?: TestRunDetection): DisciplineStage;
92
+ /**
93
+ * Whether the session's requirements grill has produced a committed spec.
94
+ * @param events - the session's append-only event log.
95
+ * @returns true when a successful `doublecheck_spec` call exists in the log;
96
+ * the scan stops at the first successful pair it finds.
97
+ */
98
+ export declare function sessionHasSpec(events: readonly SessionEvent[]): boolean;
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The dsh-doublecheck discipline state machine.
3
+ *
4
+ * Six stages: `grill` (settle requirements) → `design` (spec committed) →
5
+ * `red` (failing test) → `green` (passing test) → `review` (self-review) →
6
+ * `verify` (delivery proof). v0.1 implemented the first transition; v0.2 adds
7
+ * the red/green evidence transitions folded from durable test-run records.
8
+ *
9
+ * The state derives from the session log, never from process memory: a
10
+ * `doublecheck_spec` call with a successful result advances past `grill`, a
11
+ * failing test run marks `red`, a passing run marks `green`, and any
12
+ * implementation edit after the latest passing run re-arms the green gate.
13
+ * Resumed and forked sessions fold to the same state as the live run.
14
+ *
15
+ * @module dsh-doublecheck/domain/stages
16
+ */
17
+ import { emptyDetection, isTestCommand, isTestFilePath, joinTextBlocks, mutationTargetPath, parseRawArguments, shellCommand, testOutcome, } from "./evidence.js";
18
+ /** All discipline stages in order. */
19
+ export const DISCIPLINE_STAGES = [
20
+ 'grill', 'design', 'red', 'green', 'review', 'verify',
21
+ ];
22
+ /** Name of the model-facing tool that commits a grilled requirements spec. */
23
+ export const SPEC_TOOL_NAME = 'doublecheck_spec';
24
+ /** Name of the model-facing tool that consolidates the delivery report. */
25
+ export const REPORT_TOOL_NAME = 'doublecheck_report';
26
+ /** Which stage a successful call of the named tool advances the session to. */
27
+ const STAGE_BY_TOOL = {
28
+ [SPEC_TOOL_NAME]: 'design',
29
+ [REPORT_TOOL_NAME]: 'verify',
30
+ };
31
+ /** A fresh fold state: nothing evidenced yet. */
32
+ export function emptyDisciplineState() {
33
+ return {
34
+ stage: 'grill',
35
+ hasSpec: false,
36
+ specSeq: 0,
37
+ color: 'none',
38
+ pendingGreen: false,
39
+ editCount: 0,
40
+ pendingStageCalls: new Map(),
41
+ pendingTestCalls: new Set(),
42
+ };
43
+ }
44
+ /**
45
+ * Call ids of successful invocations of `toolName` in the given event log. A
46
+ * call is successful when some `tool/result` pairs with its `callId` and
47
+ * carries no error record. One pass over the log: results always follow
48
+ * their calls, so a pending call set pairs them in place.
49
+ * @param events - the session's append-only event log.
50
+ * @param toolName - the tool whose successful calls are collected.
51
+ * @returns every successful call id, in log order.
52
+ */
53
+ export function successfulToolCalls(events, toolName) {
54
+ const pending = new Set();
55
+ const succeeded = [];
56
+ for (const event of events) {
57
+ if (event.type === 'tool/call') {
58
+ if (event.data.name === toolName)
59
+ pending.add(event.data.callId);
60
+ }
61
+ else if (event.type === 'tool/result') {
62
+ const callId = event.data.message.source.callId;
63
+ if (event.data.error === undefined && pending.delete(callId))
64
+ succeeded.push(callId);
65
+ }
66
+ }
67
+ return succeeded;
68
+ }
69
+ /**
70
+ * Fold `events[start..end)` into the discipline state. Each event is folded
71
+ * once; the state's pending sets carry unmatched calls across batches, so the
72
+ * caller can advance an existing state over the newly appended tail instead
73
+ * of refolding the whole log.
74
+ * @param state - the state to advance (start one via {@link emptyDisciplineState}).
75
+ * @param events - the session's append-only event log.
76
+ * @param start - first index to fold.
77
+ * @param detection - the compiled red/green evidence knobs.
78
+ * @returns the same state instance, advanced.
79
+ */
80
+ export function foldDisciplineRange(state, events, start, detection) {
81
+ for (let index = start; index < events.length; index += 1) {
82
+ const event = events[index];
83
+ if (event === undefined)
84
+ continue;
85
+ switch (event.type) {
86
+ case 'tool/call': {
87
+ const args = parseRawArguments(event.data.arguments);
88
+ const stage = STAGE_BY_TOOL[event.data.name];
89
+ if (stage !== undefined) {
90
+ state.pendingStageCalls.set(event.data.callId, stage);
91
+ }
92
+ const command = shellCommand(event.data.name, args, detection);
93
+ if (command !== undefined && isTestCommand(command, detection)) {
94
+ state.pendingTestCalls.add(event.data.callId);
95
+ }
96
+ const path = mutationTargetPath(event.data.name, args, detection);
97
+ if (path !== undefined && !isTestFilePath(path, detection)) {
98
+ state.pendingGreen = true;
99
+ state.editCount += 1;
100
+ }
101
+ break;
102
+ }
103
+ case 'tool/result': {
104
+ const callId = event.data.message.source.callId;
105
+ const stage = state.pendingStageCalls.get(callId);
106
+ if (stage !== undefined) {
107
+ state.pendingStageCalls.delete(callId);
108
+ if (event.data.error === undefined) {
109
+ if (stage === 'design') {
110
+ state.hasSpec = true;
111
+ state.specSeq = event.seq;
112
+ }
113
+ state.stage = stage;
114
+ }
115
+ }
116
+ if (state.pendingTestCalls.delete(callId)) {
117
+ foldTestOutcome(state, testOutcome(joinTextBlocks(event.data.message.content), event.data.error !== undefined));
118
+ }
119
+ break;
120
+ }
121
+ case 'tool/code-dispatch': {
122
+ const args = parseRawArguments(event.data.arguments);
123
+ const command = shellCommand(event.data.name, args, detection);
124
+ if (command !== undefined && isTestCommand(command, detection)) {
125
+ foldTestOutcome(state, testOutcome(joinTextBlocks(event.data.content), event.data.isError));
126
+ }
127
+ // Code Mode dispatches run through the same pre-execute policy gates,
128
+ // so a dispatched edit is a real implementation edit and must count
129
+ // toward the green gate and the report exactly like a native call.
130
+ const path = mutationTargetPath(event.data.name, args, detection);
131
+ if (path !== undefined && !isTestFilePath(path, detection)) {
132
+ state.pendingGreen = true;
133
+ state.editCount += 1;
134
+ }
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ return state;
140
+ }
141
+ /** Apply one test-run outcome to the color and the green gate. */
142
+ function foldTestOutcome(state, outcome) {
143
+ if (outcome === 'fail') {
144
+ state.color = 'red';
145
+ state.stage = 'red';
146
+ }
147
+ else if (outcome === 'pass') {
148
+ state.color = 'green';
149
+ state.stage = 'green';
150
+ state.pendingGreen = false;
151
+ }
152
+ }
153
+ /**
154
+ * Fold a session log from scratch to its current discipline state.
155
+ * @param events - the session's append-only event log.
156
+ * @param detection - the compiled red/green evidence knobs; omit to ignore test runs.
157
+ * @returns the complete folded state.
158
+ */
159
+ export function foldDisciplineState(events, detection = emptyDetection()) {
160
+ return foldDisciplineRange(emptyDisciplineState(), events, 0, detection);
161
+ }
162
+ /**
163
+ * Fold a session log to its current discipline stage. Only successful calls
164
+ * and real test runs advance the stage; a failed spec attempt leaves the
165
+ * session in `grill`, and red/green move with the latest test evidence.
166
+ * @param events - the session's append-only event log.
167
+ * @param detection - the compiled red/green evidence knobs; omit to ignore test runs.
168
+ * @returns the last stage reached, or `grill`.
169
+ */
170
+ export function foldDisciplineStage(events, detection = emptyDetection()) {
171
+ return foldDisciplineState(events, detection).stage;
172
+ }
173
+ /**
174
+ * Whether the session's requirements grill has produced a committed spec.
175
+ * @param events - the session's append-only event log.
176
+ * @returns true when a successful `doublecheck_spec` call exists in the log;
177
+ * the scan stops at the first successful pair it finds.
178
+ */
179
+ export function sessionHasSpec(events) {
180
+ const pending = new Set();
181
+ for (const event of events) {
182
+ if (event.type === 'tool/call') {
183
+ if (event.data.name === SPEC_TOOL_NAME)
184
+ pending.add(event.data.callId);
185
+ }
186
+ else if (event.type === 'tool/result') {
187
+ if (event.data.error === undefined && pending.delete(event.data.message.source.callId))
188
+ return true;
189
+ }
190
+ }
191
+ return false;
192
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Task-vagueness detection for the discipline guard.
3
+ *
4
+ * A task is "vague" when it is brief and names no concrete artifact. Brief is
5
+ * a deployment knob (`vagueTaskMaxChars`); artifact detection is a fixed
6
+ * structural test: a file extension token, a drive-letter prefix, a
7
+ * path-separator token, a quoted keyword, an underscore keyword, or a
8
+ * hyphenated keyword. A task that names an artifact (or a URL) is concrete
9
+ * enough to edit without a grill even when it is short; a long task is
10
+ * assumed to carry its own requirements.
11
+ *
12
+ * @module dsh-doublecheck/domain/vagueness
13
+ */
14
+ /** The tunable part of the vagueness test. */
15
+ export interface VaguenessConfig {
16
+ /** Task text longer than this many characters is never considered vague. */
17
+ vagueTaskMaxChars: number;
18
+ }
19
+ /**
20
+ * Decide whether a user task statement needs a requirements grill.
21
+ * @param text - the task text as given by the user.
22
+ * @param config - the vagueness tuning.
23
+ * @returns true when the task is brief and names no concrete artifact.
24
+ */
25
+ export declare function isVagueTask(text: string, config: VaguenessConfig): boolean;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Task-vagueness detection for the discipline guard.
3
+ *
4
+ * A task is "vague" when it is brief and names no concrete artifact. Brief is
5
+ * a deployment knob (`vagueTaskMaxChars`); artifact detection is a fixed
6
+ * structural test: a file extension token, a drive-letter prefix, a
7
+ * path-separator token, a quoted keyword, an underscore keyword, or a
8
+ * hyphenated keyword. A task that names an artifact (or a URL) is concrete
9
+ * enough to edit without a grill even when it is short; a long task is
10
+ * assumed to carry its own requirements.
11
+ *
12
+ * @module dsh-doublecheck/domain/vagueness
13
+ */
14
+ /** Extension token, Windows drive prefix, or a leading path separator followed by a path body. */
15
+ const ARTIFACT_HINT = /(?:\.[A-Za-z][\w-]{0,10}(?:$|[\s"'`.,:;)\]])|[A-Za-z]:[\\/]|[\\/][^\s"'`]+)/;
16
+ /**
17
+ * A quoted keyword such as `"foo"`, `'bar'`, `` `baz` ``, “关键词” or 「关键词」.
18
+ * Requires at least one non-whitespace character between the quotes so an
19
+ * empty pair like `""` does not make a task concrete.
20
+ */
21
+ const QUOTED_HINT = /["'`“”「」『』][^"'`“”「」『』]*\S[^"'`“”「」『』]*["'`“”「」『』]/;
22
+ /**
23
+ * An identifier-style keyword containing an underscore, such as `retry_limit`
24
+ * or `user_name`. Prose rarely uses underscores, so their presence signals a
25
+ * concrete name (a variable, config key, or function) rather than a vague
26
+ * wish. Requires an alphanumeric character at each end so a bare separator
27
+ * (`_`), a pure underscore run (`___`), or a trailing `thing_` does not count.
28
+ */
29
+ const UNDERSCORE_KEYWORD = /[A-Za-z0-9][A-Za-z0-9_]*_[A-Za-z0-9_]*[A-Za-z0-9]/;
30
+ /**
31
+ * An identifier-style keyword containing a hyphen, such as `retry-limit`,
32
+ * `user-name`, or a CLI flag like `--verbose=false`'s `verbose`. Mirrors
33
+ * `UNDERSCORE_KEYWORD`: it requires an alphanumeric character at each end so
34
+ * a bare separator (`-`), a pure hyphen run (`---`), a leading `-foo`, or a
35
+ * trailing `thing-` does not count. Ordinary hyphenated compounds
36
+ * (e.g. `well-known`), dates, and numeric ranges also match; that is the
37
+ * accepted cost of recognizing config keys and option names.
38
+ */
39
+ const HYPHENATED_KEYWORD = /[A-Za-z0-9][A-Za-z0-9-]*-[A-Za-z0-9-]*[A-Za-z0-9]/;
40
+ /**
41
+ * Decide whether a user task statement needs a requirements grill.
42
+ * @param text - the task text as given by the user.
43
+ * @param config - the vagueness tuning.
44
+ * @returns true when the task is brief and names no concrete artifact.
45
+ */
46
+ export function isVagueTask(text, config) {
47
+ const normalized = text.trim();
48
+ if (normalized.length === 0)
49
+ return false;
50
+ if (normalized.length > config.vagueTaskMaxChars)
51
+ return false;
52
+ return !(ARTIFACT_HINT.test(normalized) ||
53
+ QUOTED_HINT.test(normalized) ||
54
+ UNDERSCORE_KEYWORD.test(normalized) ||
55
+ HYPHENATED_KEYWORD.test(normalized));
56
+ }