wogiflow 2.26.2 → 2.29.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/.claude/commands/wogi-bug.md +30 -0
- package/.claude/commands/wogi-debug-hypothesis.md +33 -0
- package/.claude/commands/wogi-morning.md +1 -2
- package/.claude/commands/wogi-review.md +31 -2
- package/.claude/commands/wogi-start.md +32 -0
- package/.claude/commands/wogi-statusline-setup.md +12 -0
- package/.claude/commands/wogi-story.md +3 -2
- package/.claude/docs/claude-code-compatibility.md +40 -0
- package/.claude/docs/phases/01-explore.md +2 -1
- package/.claude/docs/phases/03-implement.md +4 -0
- package/.claude/docs/phases/04-verify.md +45 -0
- package/.claude/rules/README.md +36 -0
- package/.claude/rules/_internal/worker-tool-first-turn.md +82 -0
- package/.claude/rules/alternative-execpolicy-toml-command-policy.md +11 -0
- package/.claude/rules/alternative-hand-edit-ready-json-to-register-orpha.md +11 -0
- package/.claude/rules/alternative-permission-ruleset-per-phase.md +11 -0
- package/.claude/rules/alternative-short-name.md +12 -0
- package/.claude/rules/alternative-wogi-flow-as-mcp-client-oauth-manager.md +11 -0
- package/.claude/rules/architecture/hook-three-layer.md +68 -0
- package/.claude/rules/dual-repo-architecture-2026-02-28.md +18 -0
- package/.claude/rules/github-release-workflow-2026-01-30.md +16 -0
- package/.claude/settings.json +1 -1
- package/.workflow/agents/logic-adversary.md +2 -1
- package/.workflow/agents/personas/README.md +48 -0
- package/.workflow/agents/personas/platform-rigor.md +38 -0
- package/.workflow/agents/personas/scale-skeptic.md +28 -0
- package/.workflow/agents/personas/security-hawk.md +34 -0
- package/.workflow/agents/personas/simplicity-champion.md +37 -0
- package/.workflow/agents/personas/user-advocate.md +36 -0
- package/.workflow/bridges/base-bridge.js +46 -23
- package/.workflow/templates/claude-md.hbs +44 -122
- package/.workflow/templates/partials/feature-dossiers.hbs +33 -0
- package/.workflow/templates/partials/intent-grounded-reasoning.hbs +2 -12
- package/.workflow/templates/partials/methodology-rules.hbs +85 -79
- package/.workflow/templates/tier3-dom-field-inventory.md +102 -0
- package/lib/fuzzy-patch.js +251 -0
- package/lib/installer.js +8 -0
- package/lib/memory-proposal-store.js +458 -0
- package/lib/mode-schema.js +255 -0
- package/lib/skill-proposal-store.js +432 -0
- package/lib/skill-registry.js +1 -1
- package/lib/wogi-claude +84 -9
- package/lib/wogi-claude-expect.exp +113 -76
- package/lib/workspace-channel-server.js +19 -0
- package/lib/workspace-contracts.js +1 -1
- package/lib/workspace-dispatch-tracking.js +144 -0
- package/lib/workspace-gates.js +1 -1
- package/lib/workspace-ipc-sqlite.js +550 -0
- package/lib/workspace-messages.js +92 -0
- package/lib/workspace-routing.js +1 -1
- package/lib/workspace-task-injector.js +223 -0
- package/lib/workspace.js +23 -0
- package/lib/worktree-review.js +315 -0
- package/package.json +2 -2
- package/scripts/base-workflow-step.js +1 -1
- package/scripts/flow +28 -4
- package/scripts/flow-ac-scope-preservation.js +238 -0
- package/scripts/flow-auto-review-worker.js +75 -0
- package/scripts/flow-auto-review.js +102 -0
- package/scripts/flow-autonomous-detector.js +118 -0
- package/scripts/flow-autonomous-mode.js +153 -0
- package/scripts/flow-best-of-n.js +1 -1
- package/scripts/flow-bulk-loop.js +1 -1
- package/scripts/flow-checkpoint.js +2 -6
- package/scripts/flow-community-sync.js +1 -1
- package/scripts/flow-completion-summary.js +176 -0
- package/scripts/flow-completion-truth-gate.js +343 -4
- package/scripts/flow-config-defaults.js +52 -5
- package/scripts/flow-context-compact/expander.js +1 -1
- package/scripts/flow-context-compact/section-extractor.js +2 -2
- package/scripts/flow-context-gatherer.js +1 -1
- package/scripts/flow-context-generator.js +1 -1
- package/scripts/flow-context-scoring.js +1 -1
- package/scripts/flow-correct.js +1 -1
- package/scripts/flow-decision-authority.js +66 -15
- package/scripts/flow-done.js +33 -1
- package/scripts/flow-epic-cascade.js +171 -0
- package/scripts/flow-epics.js +2 -7
- package/scripts/flow-eval-judge.js +1 -1
- package/scripts/flow-eval.js +1 -1
- package/scripts/flow-export-scanner.js +2 -6
- package/scripts/flow-failure-learning.js +1 -1
- package/scripts/flow-feature-dossier.js +787 -0
- package/scripts/flow-figma-extract.js +2 -2
- package/scripts/flow-figma-generate.js +1 -1
- package/scripts/flow-gate-confidence.js +1 -1
- package/scripts/flow-health.js +52 -1
- package/scripts/flow-hooks.js +1 -1
- package/scripts/flow-id.js +19 -3
- package/scripts/flow-instruction-richness.js +1 -1
- package/scripts/flow-knowledge-router.js +1 -1
- package/scripts/flow-knowledge-sync.js +1 -1
- package/scripts/flow-logic-adversary.js +76 -1
- package/scripts/flow-logic-rules.js +380 -0
- package/scripts/flow-long-input.js +5 -5
- package/scripts/flow-memory-sync.js +1 -1
- package/scripts/flow-memory.js +78 -7
- package/scripts/flow-migrate.js +1 -1
- package/scripts/flow-model-caller.js +1 -1
- package/scripts/flow-models.js +2 -2
- package/scripts/flow-morning.js +0 -17
- package/scripts/flow-multi-approach.js +1 -1
- package/scripts/flow-orchestrate-context.js +4 -4
- package/scripts/flow-orchestrate-templates.js +1 -1
- package/scripts/flow-orchestrate.js +8 -8
- package/scripts/flow-peer-review.js +1 -1
- package/scripts/flow-phase.js +9 -0
- package/scripts/flow-proactive-compact.js +1 -1
- package/scripts/flow-providers.js +1 -1
- package/scripts/flow-question-queue.js +255 -0
- package/scripts/flow-repo-map.js +312 -0
- package/scripts/flow-review-passes/index.js +1 -1
- package/scripts/flow-review-passes/integration.js +1 -1
- package/scripts/flow-review-passes/structure.js +1 -1
- package/scripts/flow-revision-tracker.js +1 -1
- package/scripts/flow-section-resolver.js +1 -1
- package/scripts/flow-session-end.js +74 -5
- package/scripts/flow-session-state.js +103 -1
- package/scripts/flow-setup-hooks.js +1 -1
- package/scripts/flow-skeptical-evaluator.js +274 -0
- package/scripts/flow-skill-generator.js +3 -3
- package/scripts/flow-skill-learn.js +3 -6
- package/scripts/flow-skill-manage.js +248 -0
- package/scripts/flow-spec-verifier.js +1 -1
- package/scripts/flow-standards-checker.js +75 -0
- package/scripts/flow-standards-gate.js +1 -1
- package/scripts/flow-statusline-setup.js +8 -2
- package/scripts/flow-step-changelog.js +2 -2
- package/scripts/flow-step-coverage.js +1 -1
- package/scripts/flow-step-knowledge.js +1 -1
- package/scripts/flow-step-regression.js +1 -1
- package/scripts/flow-step-simplifier.js +1 -1
- package/scripts/flow-task-analyzer.js +1 -1
- package/scripts/flow-task-classifier.js +1 -1
- package/scripts/flow-task-enforcer.js +1 -1
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-trap-zone.js +1 -1
- package/scripts/flow-utils.js +4 -0
- package/scripts/flow-worker-question-classifier.js +51 -5
- package/scripts/flow-workspace-migrate-ipc.js +216 -0
- package/scripts/flow-workspace-summary.js +256 -0
- package/scripts/hooks/adapters/base-adapter.js +2 -2
- package/scripts/hooks/core/feature-dossier-gate.js +194 -0
- package/scripts/hooks/core/observation-capture.js +24 -0
- package/scripts/hooks/core/overdue-dispatches.js +20 -1
- package/scripts/hooks/core/phase-gate.js +15 -1
- package/scripts/hooks/core/phase-transition-auto-review.js +61 -0
- package/scripts/hooks/core/post-compact.js +5 -2
- package/scripts/hooks/core/pre-tool-orchestrator.js +21 -0
- package/scripts/hooks/core/routing-gate.js +58 -0
- package/scripts/hooks/core/session-context.js +108 -0
- package/scripts/hooks/core/session-end-memory-proposals.js +65 -0
- package/scripts/hooks/core/session-end-skill-proposals.js +58 -0
- package/scripts/hooks/core/session-end.js +25 -0
- package/scripts/hooks/core/setup-handler.js +1 -1
- package/scripts/hooks/core/task-boundary-reset.js +110 -4
- package/scripts/hooks/core/worker-boundary-gate.js +71 -0
- package/scripts/hooks/core/worker-tool-first-gate.js +275 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +2 -2
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +7 -2
- package/scripts/hooks/entry/claude-code/session-start.js +74 -30
- package/scripts/hooks/entry/claude-code/stop.js +47 -1
- package/scripts/hooks/entry/claude-code/user-prompt-submit.js +17 -0
- package/.workflow/templates/partials/user-commands.hbs +0 -20
|
@@ -29,9 +29,6 @@ const { success: printSuccess, warn: printWarn } = require('./flow-output');
|
|
|
29
29
|
const CHECKPOINTS_DIR = PATHS.checkpoints;
|
|
30
30
|
const CHECKPOINT_LOG = path.join(CHECKPOINTS_DIR, 'checkpoint-log.json');
|
|
31
31
|
|
|
32
|
-
// Alias getConfig as loadConfig for minimal code changes
|
|
33
|
-
const loadConfig = getConfig;
|
|
34
|
-
|
|
35
32
|
/**
|
|
36
33
|
* Default checkpoint configuration
|
|
37
34
|
*/
|
|
@@ -385,8 +382,7 @@ function formatCheckpointList(checkpoints) {
|
|
|
385
382
|
// Module exports
|
|
386
383
|
module.exports = {
|
|
387
384
|
Checkpoint,
|
|
388
|
-
DEFAULT_CHECKPOINT_CONFIG
|
|
389
|
-
loadConfig
|
|
385
|
+
DEFAULT_CHECKPOINT_CONFIG
|
|
390
386
|
};
|
|
391
387
|
|
|
392
388
|
// CLI Handler
|
|
@@ -394,7 +390,7 @@ if (require.main === module) {
|
|
|
394
390
|
const args = process.argv.slice(2);
|
|
395
391
|
const command = args[0];
|
|
396
392
|
|
|
397
|
-
const config =
|
|
393
|
+
const config = getConfig();
|
|
398
394
|
const cp = new Checkpoint(config);
|
|
399
395
|
|
|
400
396
|
switch (command) {
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow — Autonomous-Run Completion Summary (Story C / wf-d712002e)
|
|
5
|
+
*
|
|
6
|
+
* Renders the end-of-run summary for autonomous walk-away mode in two
|
|
7
|
+
* formats:
|
|
8
|
+
* 1. Human-readable terminal block (always 3 sections: completed,
|
|
9
|
+
* queued, skipped — empty-state placeholders rendered explicitly per
|
|
10
|
+
* decisions.md 2026-04-23 "vanishing-section" rule).
|
|
11
|
+
* 2. Structured JSON payload at
|
|
12
|
+
* .workflow/state/autonomous-run-summary-<runId>.json that Story B can
|
|
13
|
+
* base64-wrap into a single-line channel-dispatch message.
|
|
14
|
+
*
|
|
15
|
+
* Programmatic:
|
|
16
|
+
* const cs = require('./flow-completion-summary');
|
|
17
|
+
* const { terminal, jsonPath } = cs.renderSummary({ runId, ... });
|
|
18
|
+
* cs.writeJsonPayload(payload); // returns the path written
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const { PATHS } = require('./flow-paths');
|
|
23
|
+
const { writeJson } = require('./flow-io');
|
|
24
|
+
|
|
25
|
+
const SEP = '━'.repeat(58);
|
|
26
|
+
|
|
27
|
+
function summaryPath(runId) {
|
|
28
|
+
return path.join(PATHS.state, `autonomous-run-summary-${runId}.json`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pad2(n) { return String(n).padStart(2, '0'); }
|
|
32
|
+
|
|
33
|
+
function formatDuration(startedAt, endedAt) {
|
|
34
|
+
if (!startedAt || !endedAt) return '0:00';
|
|
35
|
+
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
36
|
+
if (!Number.isFinite(ms) || ms < 0) return '0:00';
|
|
37
|
+
const sec = Math.floor(ms / 1000);
|
|
38
|
+
const m = Math.floor(sec / 60);
|
|
39
|
+
const s = sec % 60;
|
|
40
|
+
if (m >= 60) {
|
|
41
|
+
const h = Math.floor(m / 60);
|
|
42
|
+
return `${h}:${pad2(m % 60)}:${pad2(s)}`;
|
|
43
|
+
}
|
|
44
|
+
return `${m}:${pad2(s)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the full payload object — used for both terminal render and
|
|
49
|
+
* persisted JSON. Caller passes raw collected data; this normalizes shape.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} input
|
|
52
|
+
* @param {string} input.runId
|
|
53
|
+
* @param {string} input.startedAt
|
|
54
|
+
* @param {string} [input.endedAt]
|
|
55
|
+
* @param {string} input.trigger
|
|
56
|
+
* @param {Array<{taskId:string,title:string}>} [input.completed]
|
|
57
|
+
* @param {Array<object>} [input.queuedQuestions]
|
|
58
|
+
* @param {Array<object>} [input.skippedTasks]
|
|
59
|
+
* @param {{used:number,cap:number,breakdown?:object}} [input.adversaryInvocations]
|
|
60
|
+
* @param {string} [input.endReason] - queue-drained | user-interrupt | fatal-error
|
|
61
|
+
*/
|
|
62
|
+
function buildPayload(input) {
|
|
63
|
+
const endedAt = input.endedAt || new Date().toISOString();
|
|
64
|
+
return {
|
|
65
|
+
runId: input.runId,
|
|
66
|
+
startedAt: input.startedAt,
|
|
67
|
+
endedAt,
|
|
68
|
+
trigger: input.trigger || 'unspecified',
|
|
69
|
+
completed: Array.isArray(input.completed) ? input.completed : [],
|
|
70
|
+
queuedQuestions: Array.isArray(input.queuedQuestions) ? input.queuedQuestions : [],
|
|
71
|
+
skippedTasks: Array.isArray(input.skippedTasks) ? input.skippedTasks : [],
|
|
72
|
+
adversaryInvocations: {
|
|
73
|
+
used: input.adversaryInvocations?.used ?? 0,
|
|
74
|
+
cap: input.adversaryInvocations?.cap ?? 0,
|
|
75
|
+
breakdown: input.adversaryInvocations?.breakdown || {}
|
|
76
|
+
},
|
|
77
|
+
endReason: input.endReason || 'queue-drained'
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderTerminal(payload) {
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push(SEP);
|
|
84
|
+
lines.push(`AUTONOMOUS RUN COMPLETE (runId: ${payload.runId}, duration: ${formatDuration(payload.startedAt, payload.endedAt)})`);
|
|
85
|
+
lines.push(SEP);
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
lines.push(`✓ Completed (${payload.completed.length} tasks):`);
|
|
89
|
+
if (payload.completed.length === 0) {
|
|
90
|
+
lines.push(' [none]');
|
|
91
|
+
} else {
|
|
92
|
+
for (const t of payload.completed) {
|
|
93
|
+
lines.push(` - ${t.taskId}: ${t.title || '(no title)'}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
lines.push('');
|
|
97
|
+
|
|
98
|
+
lines.push(`? Queued questions (${payload.queuedQuestions.length}):`);
|
|
99
|
+
if (payload.queuedQuestions.length === 0) {
|
|
100
|
+
lines.push(' [none]');
|
|
101
|
+
} else {
|
|
102
|
+
for (const q of payload.queuedQuestions) {
|
|
103
|
+
const blockers = Array.isArray(q.dependencies) && q.dependencies.length
|
|
104
|
+
? ` (blocks: ${q.dependencies.join(', ')})`
|
|
105
|
+
: '';
|
|
106
|
+
lines.push(` - ${q.id}: ${q.text}${blockers}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
lines.push('');
|
|
110
|
+
|
|
111
|
+
lines.push(`⊘ Skipped tasks (${payload.skippedTasks.length}):`);
|
|
112
|
+
if (payload.skippedTasks.length === 0) {
|
|
113
|
+
lines.push(' [none]');
|
|
114
|
+
} else {
|
|
115
|
+
for (const s of payload.skippedTasks) {
|
|
116
|
+
const ref = s.blockingQuestionId ? ` (awaiting answer to ${s.blockingQuestionId})` : '';
|
|
117
|
+
lines.push(` - ${s.taskId}: ${s.reason}${ref}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
lines.push('');
|
|
121
|
+
|
|
122
|
+
lines.push(`⚡ Adversary invocations: ${payload.adversaryInvocations.used} / ${payload.adversaryInvocations.cap} (cap)`);
|
|
123
|
+
lines.push(` reason: ${payload.endReason}`);
|
|
124
|
+
lines.push(SEP);
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Persist the JSON payload to .workflow/state/autonomous-run-summary-<runId>.json.
|
|
130
|
+
* Returns the absolute path written.
|
|
131
|
+
*/
|
|
132
|
+
function writeJsonPayload(payload) {
|
|
133
|
+
const p = summaryPath(payload.runId);
|
|
134
|
+
writeJson(p, payload);
|
|
135
|
+
return p;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function renderSummary(input, { writeJson: write = true } = {}) {
|
|
139
|
+
const payload = buildPayload(input);
|
|
140
|
+
const terminal = renderTerminal(payload);
|
|
141
|
+
let jsonPath = null;
|
|
142
|
+
if (write) {
|
|
143
|
+
jsonPath = writeJsonPayload(payload);
|
|
144
|
+
}
|
|
145
|
+
return { payload, terminal, jsonPath };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
buildPayload,
|
|
150
|
+
renderTerminal,
|
|
151
|
+
writeJsonPayload,
|
|
152
|
+
renderSummary,
|
|
153
|
+
summaryPath
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (require.main === module) {
|
|
157
|
+
const sample = {
|
|
158
|
+
runId: 'sample',
|
|
159
|
+
startedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
160
|
+
trigger: 'go until you finish',
|
|
161
|
+
completed: [
|
|
162
|
+
{ taskId: 'wf-aaaaaaaa', title: 'Add team-id to API' },
|
|
163
|
+
{ taskId: 'wf-bbbbbbbb', title: 'Update changelog' }
|
|
164
|
+
],
|
|
165
|
+
queuedQuestions: [
|
|
166
|
+
{ id: 'q-12345678', text: 'Should admins be charged?', dependencies: ['wf-cccccccc'] }
|
|
167
|
+
],
|
|
168
|
+
skippedTasks: [
|
|
169
|
+
{ taskId: 'wf-cccccccc', reason: 'awaiting answer', blockingQuestionId: 'q-12345678' }
|
|
170
|
+
],
|
|
171
|
+
adversaryInvocations: { used: 4, cap: 30 },
|
|
172
|
+
endReason: 'queue-drained'
|
|
173
|
+
};
|
|
174
|
+
const { terminal } = renderSummary(sample, { writeJson: false });
|
|
175
|
+
console.log(terminal);
|
|
176
|
+
}
|
|
@@ -52,7 +52,7 @@ const gateTelemetry = require('./flow-gate-telemetry');
|
|
|
52
52
|
|
|
53
53
|
// Lazy-loaded to keep Story 6 independently testable
|
|
54
54
|
let _evidenceTiers;
|
|
55
|
-
function
|
|
55
|
+
function _getEvidenceTiers() {
|
|
56
56
|
if (_evidenceTiers) return _evidenceTiers;
|
|
57
57
|
try {
|
|
58
58
|
_evidenceTiers = require('./flow-runtime-verification').EVIDENCE_TIERS;
|
|
@@ -282,7 +282,7 @@ function auditCompletionClaim(taskId, claimedCriteria) {
|
|
|
282
282
|
const insufficient = perCriterion.filter((p) => p.claimedDone && !p.sufficient);
|
|
283
283
|
const sufficient = perCriterion.filter((p) => p.claimedDone && p.sufficient);
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
const audit = {
|
|
286
286
|
perCriterion,
|
|
287
287
|
blocked: insufficient.length > 0 && shouldBlockOnFalseCompletion(),
|
|
288
288
|
softModeWarn: insufficient.length > 0 && !shouldBlockOnFalseCompletion(),
|
|
@@ -292,6 +292,25 @@ function auditCompletionClaim(taskId, claimedCriteria) {
|
|
|
292
292
|
totalClaimed: perCriterion.filter((p) => p.claimedDone).length,
|
|
293
293
|
evidenceRecordsExisted,
|
|
294
294
|
};
|
|
295
|
+
|
|
296
|
+
// wf-8d635d0e / E1: fold parallel-worktree auto-review findings into the
|
|
297
|
+
// audit. High-severity findings flip softModeWarn on so downgradeClaim
|
|
298
|
+
// rewrites language. Timeout → informational note only (AC5).
|
|
299
|
+
try {
|
|
300
|
+
const { summarizeFindingsForAudit } = require('../lib/worktree-review');
|
|
301
|
+
const summary = summarizeFindingsForAudit(taskId);
|
|
302
|
+
if (summary.present) {
|
|
303
|
+
audit.autoReview = summary;
|
|
304
|
+
if (summary.highSeverityCount > 0) {
|
|
305
|
+
audit.softModeWarn = true;
|
|
306
|
+
}
|
|
307
|
+
if (summary.timedOut) {
|
|
308
|
+
audit.softModeWarn = audit.softModeWarn || false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (_err) { /* fail-open — auto-review is advisory */ }
|
|
312
|
+
|
|
313
|
+
return audit;
|
|
295
314
|
}
|
|
296
315
|
|
|
297
316
|
function normalizeText(s) {
|
|
@@ -325,10 +344,25 @@ function downgradeClaim(originalText, audit) {
|
|
|
325
344
|
const re = new RegExp(`\\b(${DONE_WORDS.join('|')})\\b`, 'gi');
|
|
326
345
|
const rewritten = String(originalText || '').replace(re, downgradedWord);
|
|
327
346
|
|
|
328
|
-
|
|
347
|
+
let banner =
|
|
329
348
|
`\n\n⚠ Completion Truth Gate: ${sufficientCount}/${totalClaimed} criteria reach the required ${tierName} (≥ Tier ${minTier}) evidence threshold. ` +
|
|
330
349
|
`${insufficientCount} criteria are implemented but unverified — recommend manual verification before announcing completion.`;
|
|
331
350
|
|
|
351
|
+
// Auto-review findings addendum (wf-8d635d0e).
|
|
352
|
+
if (audit.autoReview && audit.autoReview.present) {
|
|
353
|
+
const ar = audit.autoReview;
|
|
354
|
+
if (ar.timedOut) {
|
|
355
|
+
banner += `\n⏱ Auto-review timed out (unverified-review-timeout) — no blocking signal from background review.`;
|
|
356
|
+
} else if (ar.highSeverityCount > 0) {
|
|
357
|
+
banner += `\n🔴 Auto-review: ${ar.highSeverityCount} high-severity finding(s) on changed lines.`;
|
|
358
|
+
for (const f of ar.topHighSeverity) {
|
|
359
|
+
banner += `\n - ${f.file || '?'}:${f.line || 0} — ${f.claim}`;
|
|
360
|
+
}
|
|
361
|
+
} else if ((ar.counts?.medium || 0) + (ar.counts?.low || 0) > 0) {
|
|
362
|
+
banner += `\n📝 Auto-review: ${ar.counts.medium} medium, ${ar.counts.low} low severity (informational).`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
332
366
|
return {
|
|
333
367
|
text: rewritten + banner,
|
|
334
368
|
replaced: rewritten !== originalText,
|
|
@@ -477,7 +511,7 @@ const NEGATION_PREFIXES = /\b(?:no|zero|0|without(?: any)?|not a single)\s+/i;
|
|
|
477
511
|
* not match.
|
|
478
512
|
*/
|
|
479
513
|
const DISAGREEMENT_WORDS = ['outage', 'outages', 'incident', 'incidents', 'regression', 'regressions', 'rollback', 'rollbacks', 'revert', 'reverts', 'hotfix', 'hotfixes'];
|
|
480
|
-
const
|
|
514
|
+
const _DISAGREEMENT_RE = new RegExp(`\\b(?:${DISAGREEMENT_WORDS.join('|')})\\b`, 'i');
|
|
481
515
|
|
|
482
516
|
const PARTIAL_STATUSES = new Set(['completed-partial', 'completed_partial', 'partial', 'in-progress', 'in_progress', 'blocked', 'failed']);
|
|
483
517
|
|
|
@@ -741,6 +775,302 @@ function formatMissingClaimsMessage(result) {
|
|
|
741
775
|
return lines.join('\n');
|
|
742
776
|
}
|
|
743
777
|
|
|
778
|
+
// ============================================================
|
|
779
|
+
// Spec-String Bundle Grep (wf-07046456 / B4)
|
|
780
|
+
// ============================================================
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Extract the "string bundle" from a spec: every named artifact the spec promises
|
|
784
|
+
* to produce, consume, or reference. The bundle is the set of concrete strings
|
|
785
|
+
* that MUST appear somewhere in the delivery (diff, changed files, bundle output)
|
|
786
|
+
* for the spec to be honored.
|
|
787
|
+
*
|
|
788
|
+
* Bundles extracted:
|
|
789
|
+
* - Backtick-quoted identifiers: `functionName`, `ConfigKey`, `module/path.js`
|
|
790
|
+
* - Double-quoted string literals: "exact error message", "button label"
|
|
791
|
+
* - File paths with extensions: foo/bar.js, .workflow/state/X.json
|
|
792
|
+
* - ALLCAPS_CONSTANTS
|
|
793
|
+
* - Route/URL paths: /api/v1/users, /dashboard/settings
|
|
794
|
+
*
|
|
795
|
+
* @param {string} specMarkdown
|
|
796
|
+
* @returns {{ backtickIds: string[], quotedStrings: string[], filePaths: string[], constants: string[], routes: string[], all: string[] }}
|
|
797
|
+
*/
|
|
798
|
+
function extractSpecStrings(specMarkdown) {
|
|
799
|
+
if (typeof specMarkdown !== 'string' || specMarkdown.length === 0) {
|
|
800
|
+
return { backtickIds: [], quotedStrings: [], filePaths: [], constants: [], routes: [], all: [] };
|
|
801
|
+
}
|
|
802
|
+
// Strip code fences first so we don't double-count bodies of code blocks
|
|
803
|
+
const withoutFences = specMarkdown.replace(/```[\s\S]*?```/g, '');
|
|
804
|
+
|
|
805
|
+
const backtickIds = [...new Set(
|
|
806
|
+
[...withoutFences.matchAll(/`([^`\n]{2,80})`/g)].map((m) => m[1].trim())
|
|
807
|
+
.filter((s) => s.length >= 2 && !/^\s*$/.test(s))
|
|
808
|
+
)];
|
|
809
|
+
const quotedStrings = [...new Set(
|
|
810
|
+
[...withoutFences.matchAll(/"([^"\n]{3,120})"/g)].map((m) => m[1].trim())
|
|
811
|
+
.filter((s) => !/^(TODO|FIXME|XXX)$/i.test(s))
|
|
812
|
+
)];
|
|
813
|
+
const filePaths = [...new Set(
|
|
814
|
+
[...withoutFences.matchAll(/\b([\w./-]+\.(?:js|ts|tsx|jsx|md|json|yaml|yml|py|go|rs|sh|toml|hbs))\b/g)].map((m) => m[1])
|
|
815
|
+
)];
|
|
816
|
+
const constants = [...new Set(
|
|
817
|
+
// Require at least one underscore or digit — excludes bare HTTP verbs (POST/GET) and common all-caps words (JSON/HTML/CSV)
|
|
818
|
+
[...withoutFences.matchAll(/\b([A-Z][A-Z0-9]*_[A-Z0-9_]{1,40}|[A-Z_]{2,}\d+[A-Z0-9_]*)\b/g)].map((m) => m[1])
|
|
819
|
+
.filter((s) => !/^(TODO|FIXME|XXX|NOTE|HACK|TBD|WIP)$/.test(s))
|
|
820
|
+
)];
|
|
821
|
+
const routes = [...new Set(
|
|
822
|
+
[...withoutFences.matchAll(/(?:^|\s|`)(\/[a-z0-9][a-z0-9./_:-]{2,80})(?=[\s`"'.,]|$)/gmi)].map((m) => m[1])
|
|
823
|
+
.filter((s) => !s.startsWith('//') && !s.startsWith('/Users/') && !s.startsWith('/home/'))
|
|
824
|
+
)];
|
|
825
|
+
|
|
826
|
+
const all = [...new Set([...backtickIds, ...quotedStrings, ...filePaths, ...constants, ...routes])];
|
|
827
|
+
return { backtickIds, quotedStrings, filePaths, constants, routes, all };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Verify spec-string coverage against delivery.
|
|
832
|
+
*
|
|
833
|
+
* @param {object} opts
|
|
834
|
+
* @param {string} opts.specMarkdown
|
|
835
|
+
* @param {string} opts.diffText - git diff
|
|
836
|
+
* @param {string[]} [opts.changedFiles]
|
|
837
|
+
* @param {string} [opts.bundleText] - built-bundle text (minified) if available
|
|
838
|
+
* @param {string[]} [opts.additionalSources] - other text sources (e.g., commit message)
|
|
839
|
+
* @param {object} [opts.categoryMins] - minimum coverage ratio per category (default 0.8)
|
|
840
|
+
* @returns {{ ok: boolean, missingByCategory: object, coverage: object, strict: boolean }}
|
|
841
|
+
*/
|
|
842
|
+
function verifySpecBundleCoverage({
|
|
843
|
+
specMarkdown,
|
|
844
|
+
diffText = '',
|
|
845
|
+
changedFiles = [],
|
|
846
|
+
bundleText = '',
|
|
847
|
+
additionalSources = [],
|
|
848
|
+
categoryMins = {},
|
|
849
|
+
}) {
|
|
850
|
+
const bundle = extractSpecStrings(specMarkdown);
|
|
851
|
+
const haystack = [diffText, bundleText, changedFiles.join('\n'), ...additionalSources].join('\n');
|
|
852
|
+
const defaults = { backtickIds: 0.8, quotedStrings: 0.7, filePaths: 1.0, constants: 0.8, routes: 1.0 };
|
|
853
|
+
const mins = { ...defaults, ...categoryMins };
|
|
854
|
+
|
|
855
|
+
const coverage = {};
|
|
856
|
+
const missingByCategory = {};
|
|
857
|
+
for (const cat of Object.keys(mins)) {
|
|
858
|
+
const items = bundle[cat] || [];
|
|
859
|
+
if (items.length === 0) { coverage[cat] = { total: 0, hit: 0, ratio: 1, threshold: mins[cat] }; missingByCategory[cat] = []; continue; }
|
|
860
|
+
const missing = items.filter((s) => !haystack.includes(s));
|
|
861
|
+
const hit = items.length - missing.length;
|
|
862
|
+
coverage[cat] = { total: items.length, hit, ratio: hit / items.length, threshold: mins[cat], missing };
|
|
863
|
+
missingByCategory[cat] = missing;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const strict = Object.entries(coverage).every(([, v]) => v.ratio >= v.threshold);
|
|
867
|
+
return { ok: strict, missingByCategory, coverage, strict };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Format spec-bundle verification as a human-readable report.
|
|
872
|
+
* @param {object} result
|
|
873
|
+
* @returns {string|null}
|
|
874
|
+
*/
|
|
875
|
+
function formatSpecBundleResult(result) {
|
|
876
|
+
if (!result) return null;
|
|
877
|
+
const lines = [];
|
|
878
|
+
lines.push(result.ok ? 'Spec-bundle grep OK:' : 'Spec-bundle grep FAIL:');
|
|
879
|
+
for (const [cat, v] of Object.entries(result.coverage)) {
|
|
880
|
+
if (v.total === 0) continue;
|
|
881
|
+
const mark = v.ratio >= v.threshold ? '✓' : '✗';
|
|
882
|
+
lines.push(` ${mark} ${cat}: ${v.hit}/${v.total} (ratio ${v.ratio.toFixed(2)}, needs ${v.threshold.toFixed(2)})`);
|
|
883
|
+
if (v.ratio < v.threshold && v.missing.length > 0) {
|
|
884
|
+
lines.push(` missing: ${v.missing.slice(0, 6).map((s) => `"${s}"`).join(', ')}${v.missing.length > 6 ? ', ...' : ''}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return lines.join('\n');
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ============================================================
|
|
891
|
+
// BEL-file grep (wf-10c452f7 / B2) — Bulleted-Expectation List grep
|
|
892
|
+
// ============================================================
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Parse a spec file for BEL (bulleted-expectation list) items. A BEL item is any
|
|
896
|
+
* top-level `- ` or `* ` bullet under an "Acceptance Criteria", "Expectations",
|
|
897
|
+
* "Requirements", or "Success Criteria" heading.
|
|
898
|
+
*
|
|
899
|
+
* @param {string} specMarkdown
|
|
900
|
+
* @returns {Array<{text: string, heading: string}>}
|
|
901
|
+
*/
|
|
902
|
+
function parseBELItems(specMarkdown) {
|
|
903
|
+
if (typeof specMarkdown !== 'string' || specMarkdown.length === 0) return [];
|
|
904
|
+
const lines = specMarkdown.split('\n');
|
|
905
|
+
const belHeadingRe = /^(#{1,6})\s+(Acceptance Criteria|Expectations|Requirements|Success Criteria|Acceptance|Definition of Done|Criteria)\b/i;
|
|
906
|
+
const anyHeadingRe = /^#{1,6}\s+/;
|
|
907
|
+
const bulletRe = /^\s*[-*]\s+(.+)$/;
|
|
908
|
+
const items = [];
|
|
909
|
+
|
|
910
|
+
let inSection = false;
|
|
911
|
+
let currentHeading = '';
|
|
912
|
+
for (const line of lines) {
|
|
913
|
+
const belMatch = line.match(belHeadingRe);
|
|
914
|
+
if (belMatch) { inSection = true; currentHeading = belMatch[2]; continue; }
|
|
915
|
+
if (inSection && anyHeadingRe.test(line) && !line.match(belHeadingRe)) { inSection = false; continue; }
|
|
916
|
+
if (!inSection) continue;
|
|
917
|
+
const b = line.match(bulletRe);
|
|
918
|
+
if (b) items.push({ text: b[1].trim(), heading: currentHeading });
|
|
919
|
+
}
|
|
920
|
+
return items;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Extract keyword tokens from a BEL item for grep-based coverage detection.
|
|
925
|
+
* Reuses the STOPWORDS heuristic used elsewhere.
|
|
926
|
+
* @param {string} text
|
|
927
|
+
* @returns {string[]}
|
|
928
|
+
*/
|
|
929
|
+
function _belKeywords(text) {
|
|
930
|
+
const STOPWORDS = new Set([
|
|
931
|
+
'with', 'from', 'that', 'this', 'have', 'make', 'been', 'were', 'their',
|
|
932
|
+
'they', 'them', 'will', 'should', 'would', 'could', 'there', 'into',
|
|
933
|
+
'when', 'then', 'than', 'which', 'what', 'your', 'user', 'users', 'given',
|
|
934
|
+
'able', 'must', 'shall', 'system', 'application', 'feature',
|
|
935
|
+
]);
|
|
936
|
+
const tokens = String(text).toLowerCase().match(/\b[a-z][a-z0-9_-]{3,}\b/g) || [];
|
|
937
|
+
return [...new Set(tokens.filter((t) => !STOPWORDS.has(t)))];
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Verify each BEL item's keywords appear somewhere in the delivery haystack
|
|
942
|
+
* (commit message + diff + changed-file paths).
|
|
943
|
+
*
|
|
944
|
+
* @param {object} opts
|
|
945
|
+
* @param {string} opts.specMarkdown - the spec content to parse BEL items from
|
|
946
|
+
* @param {string} opts.diffText - output of `git diff` or equivalent
|
|
947
|
+
* @param {string[]} [opts.changedFiles] - changed-file paths
|
|
948
|
+
* @param {string} [opts.commitMessage] - commit message
|
|
949
|
+
* @param {number} [opts.minKeywordHits=2] - min distinct keyword hits per item for coverage
|
|
950
|
+
* @returns {{ ok: boolean, totalItems: number, coveredItems: Array, uncoveredItems: Array }}
|
|
951
|
+
*/
|
|
952
|
+
function verifyBELAgainstDelivery({ specMarkdown, diffText, changedFiles = [], commitMessage = '', minKeywordHits = 2 }) {
|
|
953
|
+
const items = parseBELItems(specMarkdown);
|
|
954
|
+
if (items.length === 0) return { ok: true, totalItems: 0, coveredItems: [], uncoveredItems: [] };
|
|
955
|
+
|
|
956
|
+
const haystack = [diffText || '', commitMessage || '', changedFiles.join(' ')].join('\n').toLowerCase();
|
|
957
|
+
const covered = [];
|
|
958
|
+
const uncovered = [];
|
|
959
|
+
|
|
960
|
+
for (const item of items) {
|
|
961
|
+
const keywords = _belKeywords(item.text);
|
|
962
|
+
if (keywords.length === 0) { covered.push({ ...item, hits: 0, keywords: [] }); continue; }
|
|
963
|
+
|
|
964
|
+
const hits = keywords.filter((k) => haystack.includes(k));
|
|
965
|
+
const threshold = Math.min(minKeywordHits, keywords.length);
|
|
966
|
+
if (hits.length >= threshold) {
|
|
967
|
+
covered.push({ ...item, hits: hits.length, keywords, matchedKeywords: hits });
|
|
968
|
+
} else {
|
|
969
|
+
uncovered.push({ ...item, hits: hits.length, keywords, matchedKeywords: hits, threshold });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return {
|
|
974
|
+
ok: uncovered.length === 0,
|
|
975
|
+
totalItems: items.length,
|
|
976
|
+
coveredItems: covered,
|
|
977
|
+
uncoveredItems: uncovered,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Format BEL verification result as a human-readable report.
|
|
983
|
+
* @param {object} result
|
|
984
|
+
* @param {string} [specPath]
|
|
985
|
+
* @returns {string|null}
|
|
986
|
+
*/
|
|
987
|
+
function formatBELResult(result, specPath = '') {
|
|
988
|
+
if (!result || result.totalItems === 0) return null;
|
|
989
|
+
if (result.ok) {
|
|
990
|
+
return `BEL gate OK: all ${result.totalItems} expectation(s) from ${specPath || 'spec'} covered by delivery.`;
|
|
991
|
+
}
|
|
992
|
+
const lines = [`BEL gate FAIL: ${result.uncoveredItems.length}/${result.totalItems} expectation(s) not found in delivery (${specPath || 'spec'}):`];
|
|
993
|
+
for (const u of result.uncoveredItems) {
|
|
994
|
+
lines.push(` ✗ [${u.heading}] "${u.text.slice(0, 80)}"`);
|
|
995
|
+
lines.push(` matched ${u.hits}/${u.keywords.length} keywords (need ${u.threshold}); missing: ${u.keywords.filter((k) => !u.matchedKeywords.includes(k)).slice(0, 5).join(', ')}`);
|
|
996
|
+
}
|
|
997
|
+
lines.push('');
|
|
998
|
+
lines.push('Options: add the missing implementation, update the spec if the expectation was dropped with user approval, or force with --skip-bel.');
|
|
999
|
+
return lines.join('\n');
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ============================================================
|
|
1003
|
+
// Confidence-Tier Rubric (95 / 85 / 75)
|
|
1004
|
+
// See .workflow/rubrics/confidence-tiers.md for full rubric.
|
|
1005
|
+
// Reconciled with EVIDENCE_TIERS (0..4). Story: wf-f14dcfeb (A4).
|
|
1006
|
+
// ============================================================
|
|
1007
|
+
|
|
1008
|
+
const CONFIDENCE_TIERS = { HIGH: 95, MEDIUM: 85, LOW: 75 };
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Map (evidenceTier, signal strength) → confidencePct per the rubric.
|
|
1012
|
+
*
|
|
1013
|
+
* @param {Object} opts
|
|
1014
|
+
* @param {number} opts.evidenceTier - 0..4 (see EVIDENCE_TIERS)
|
|
1015
|
+
* @param {number} [opts.hitCount] - grep/glob match count (for tier 1)
|
|
1016
|
+
* @param {number} [opts.fileCount] - distinct files for tier 1 hits
|
|
1017
|
+
* @param {number} [opts.observationCount] - corroborating observations (for tier 2)
|
|
1018
|
+
* @param {boolean} [opts.hasEvidenceNote] - whether a concrete citation was provided
|
|
1019
|
+
* @returns {{ confidencePct: 95|85|75, flagUnverified: boolean, severityCap: 'LOW'|'HIGH'|null, rationale: string }}
|
|
1020
|
+
*/
|
|
1021
|
+
function computeConfidenceTier({
|
|
1022
|
+
evidenceTier,
|
|
1023
|
+
hitCount = 0,
|
|
1024
|
+
fileCount = 0,
|
|
1025
|
+
observationCount = 0,
|
|
1026
|
+
hasEvidenceNote = true,
|
|
1027
|
+
} = {}) {
|
|
1028
|
+
const t = typeof evidenceTier === 'number' ? evidenceTier : -1;
|
|
1029
|
+
|
|
1030
|
+
if (t >= 3) {
|
|
1031
|
+
return { confidencePct: 95, flagUnverified: false, severityCap: null, rationale: 'tier >= 3 (interactive/automated)' };
|
|
1032
|
+
}
|
|
1033
|
+
if (t === 2) {
|
|
1034
|
+
if (observationCount >= 2) {
|
|
1035
|
+
return { confidencePct: 95, flagUnverified: false, severityCap: null, rationale: 'tier 2 with 2+ corroborating observations' };
|
|
1036
|
+
}
|
|
1037
|
+
return { confidencePct: 85, flagUnverified: false, severityCap: 'HIGH', rationale: 'tier 2, single observation' };
|
|
1038
|
+
}
|
|
1039
|
+
if (t === 1) {
|
|
1040
|
+
if (hitCount >= 10 && fileCount >= 3) {
|
|
1041
|
+
return { confidencePct: 95, flagUnverified: false, severityCap: null, rationale: 'tier 1 with 10+ hits across 3+ files' };
|
|
1042
|
+
}
|
|
1043
|
+
if (hitCount >= 5) {
|
|
1044
|
+
return { confidencePct: 85, flagUnverified: false, severityCap: 'HIGH', rationale: 'tier 1 with 5-9 hits' };
|
|
1045
|
+
}
|
|
1046
|
+
if (hitCount >= 3 && fileCount >= 2) {
|
|
1047
|
+
return { confidencePct: 85, flagUnverified: false, severityCap: 'HIGH', rationale: 'tier 1 with 3+ hits across 2+ files' };
|
|
1048
|
+
}
|
|
1049
|
+
return { confidencePct: 75, flagUnverified: true, severityCap: 'LOW', rationale: 'tier 1, isolated hits' };
|
|
1050
|
+
}
|
|
1051
|
+
if (t === 0 || !hasEvidenceNote) {
|
|
1052
|
+
return { confidencePct: 75, flagUnverified: true, severityCap: 'LOW', rationale: 'tier 0 (static/source-inference) or no evidenceNote' };
|
|
1053
|
+
}
|
|
1054
|
+
// t === -1: no evidence
|
|
1055
|
+
return { confidencePct: 75, flagUnverified: true, severityCap: 'LOW', rationale: 'no evidence recorded' };
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Validate a finding carries confidencePct in the allowed set.
|
|
1060
|
+
* @param {Object} finding
|
|
1061
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
1062
|
+
*/
|
|
1063
|
+
function validateConfidencePct(finding) {
|
|
1064
|
+
const allowed = [95, 85, 75];
|
|
1065
|
+
if (!finding || !allowed.includes(finding.confidencePct)) {
|
|
1066
|
+
return { ok: false, reason: `confidencePct must be one of ${allowed.join('|')}; got ${finding?.confidencePct}` };
|
|
1067
|
+
}
|
|
1068
|
+
if (finding.confidencePct === 75 && !finding.flagUnverified) {
|
|
1069
|
+
return { ok: false, reason: 'confidencePct=75 findings MUST set flagUnverified=true' };
|
|
1070
|
+
}
|
|
1071
|
+
return { ok: true };
|
|
1072
|
+
}
|
|
1073
|
+
|
|
744
1074
|
// ============================================================
|
|
745
1075
|
// Exports
|
|
746
1076
|
// ============================================================
|
|
@@ -757,6 +1087,15 @@ module.exports = {
|
|
|
757
1087
|
parseCommitMessageClaims,
|
|
758
1088
|
verifyCommitMessageAgainstDiff,
|
|
759
1089
|
formatMissingClaimsMessage,
|
|
1090
|
+
computeConfidenceTier,
|
|
1091
|
+
validateConfidencePct,
|
|
1092
|
+
CONFIDENCE_TIERS,
|
|
1093
|
+
parseBELItems,
|
|
1094
|
+
verifyBELAgainstDelivery,
|
|
1095
|
+
formatBELResult,
|
|
1096
|
+
extractSpecStrings,
|
|
1097
|
+
verifySpecBundleCoverage,
|
|
1098
|
+
formatSpecBundleResult,
|
|
760
1099
|
TIER_NAMES,
|
|
761
1100
|
DONE_WORDS,
|
|
762
1101
|
DISAGREEMENT_WORDS,
|