opencastle 0.31.6 → 0.32.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/LICENSE +93 -21
- package/README.md +9 -3
- package/bin/cli.mjs +15 -0
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +19 -5
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/artifacts-cli.d.ts +3 -0
- package/dist/cli/artifacts-cli.d.ts.map +1 -0
- package/dist/cli/artifacts-cli.js +36 -0
- package/dist/cli/artifacts-cli.js.map +1 -0
- package/dist/cli/baselines.d.ts.map +1 -1
- package/dist/cli/baselines.js +11 -0
- package/dist/cli/baselines.js.map +1 -1
- package/dist/cli/convoy/artifacts.d.ts +25 -0
- package/dist/cli/convoy/artifacts.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.js +129 -0
- package/dist/cli/convoy/artifacts.js.map +1 -0
- package/dist/cli/convoy/artifacts.test.d.ts +2 -0
- package/dist/cli/convoy/artifacts.test.d.ts.map +1 -0
- package/dist/cli/convoy/artifacts.test.js +169 -0
- package/dist/cli/convoy/artifacts.test.js.map +1 -0
- package/dist/cli/convoy/compaction.d.ts +23 -0
- package/dist/cli/convoy/compaction.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.js +117 -0
- package/dist/cli/convoy/compaction.js.map +1 -0
- package/dist/cli/convoy/compaction.test.d.ts +2 -0
- package/dist/cli/convoy/compaction.test.d.ts.map +1 -0
- package/dist/cli/convoy/compaction.test.js +205 -0
- package/dist/cli/convoy/compaction.test.js.map +1 -0
- package/dist/cli/convoy/contracts.d.ts +22 -0
- package/dist/cli/convoy/contracts.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.js +254 -0
- package/dist/cli/convoy/contracts.js.map +1 -0
- package/dist/cli/convoy/contracts.test.d.ts +2 -0
- package/dist/cli/convoy/contracts.test.d.ts.map +1 -0
- package/dist/cli/convoy/contracts.test.js +239 -0
- package/dist/cli/convoy/contracts.test.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.d.ts +40 -0
- package/dist/cli/convoy/dag-analysis.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.js +282 -0
- package/dist/cli/convoy/dag-analysis.js.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts +2 -0
- package/dist/cli/convoy/dag-analysis.test.d.ts.map +1 -0
- package/dist/cli/convoy/dag-analysis.test.js +289 -0
- package/dist/cli/convoy/dag-analysis.test.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.d.ts +20 -0
- package/dist/cli/convoy/effort-scaling.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.js +82 -0
- package/dist/cli/convoy/effort-scaling.js.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts +2 -0
- package/dist/cli/convoy/effort-scaling.test.d.ts.map +1 -0
- package/dist/cli/convoy/effort-scaling.test.js +120 -0
- package/dist/cli/convoy/effort-scaling.test.js.map +1 -0
- package/dist/cli/convoy/engine.d.ts.map +1 -1
- package/dist/cli/convoy/engine.js +298 -11
- package/dist/cli/convoy/engine.js.map +1 -1
- package/dist/cli/convoy/engine.test.js +155 -18
- package/dist/cli/convoy/engine.test.js.map +1 -1
- package/dist/cli/convoy/event-schemas.d.ts.map +1 -1
- package/dist/cli/convoy/event-schemas.js +55 -0
- package/dist/cli/convoy/event-schemas.js.map +1 -1
- package/dist/cli/convoy/isolation.d.ts +27 -0
- package/dist/cli/convoy/isolation.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.js +120 -0
- package/dist/cli/convoy/isolation.js.map +1 -0
- package/dist/cli/convoy/isolation.test.d.ts +2 -0
- package/dist/cli/convoy/isolation.test.d.ts.map +1 -0
- package/dist/cli/convoy/isolation.test.js +105 -0
- package/dist/cli/convoy/isolation.test.js.map +1 -0
- package/dist/cli/convoy/review-stages.d.ts +9 -0
- package/dist/cli/convoy/review-stages.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.js +134 -0
- package/dist/cli/convoy/review-stages.js.map +1 -0
- package/dist/cli/convoy/review-stages.test.d.ts +2 -0
- package/dist/cli/convoy/review-stages.test.d.ts.map +1 -0
- package/dist/cli/convoy/review-stages.test.js +197 -0
- package/dist/cli/convoy/review-stages.test.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.d.ts +39 -0
- package/dist/cli/convoy/skill-refinement.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.js +239 -0
- package/dist/cli/convoy/skill-refinement.js.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts +2 -0
- package/dist/cli/convoy/skill-refinement.test.d.ts.map +1 -0
- package/dist/cli/convoy/skill-refinement.test.js +230 -0
- package/dist/cli/convoy/skill-refinement.test.js.map +1 -0
- package/dist/cli/convoy/spec-builder.d.ts +1 -0
- package/dist/cli/convoy/spec-builder.d.ts.map +1 -1
- package/dist/cli/convoy/spec-builder.js +11 -0
- package/dist/cli/convoy/spec-builder.js.map +1 -1
- package/dist/cli/convoy/spec-builder.test.js +54 -0
- package/dist/cli/convoy/spec-builder.test.js.map +1 -1
- package/dist/cli/convoy/store.d.ts +3 -2
- package/dist/cli/convoy/store.d.ts.map +1 -1
- package/dist/cli/convoy/store.js +20 -2
- package/dist/cli/convoy/store.js.map +1 -1
- package/dist/cli/convoy/store.test.js +15 -15
- package/dist/cli/convoy/store.test.js.map +1 -1
- package/dist/cli/convoy/tdd-gate.d.ts +15 -0
- package/dist/cli/convoy/tdd-gate.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.js +119 -0
- package/dist/cli/convoy/tdd-gate.js.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts +2 -0
- package/dist/cli/convoy/tdd-gate.test.d.ts.map +1 -0
- package/dist/cli/convoy/tdd-gate.test.js +227 -0
- package/dist/cli/convoy/tdd-gate.test.js.map +1 -0
- package/dist/cli/convoy/types.d.ts +91 -0
- package/dist/cli/convoy/types.d.ts.map +1 -1
- package/dist/cli/convoy/types.js +8 -0
- package/dist/cli/convoy/types.js.map +1 -1
- package/dist/cli/dashboard.d.ts.map +1 -1
- package/dist/cli/dashboard.js +54 -0
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/insights.d.ts +3 -0
- package/dist/cli/insights.d.ts.map +1 -0
- package/dist/cli/insights.js +94 -0
- package/dist/cli/insights.js.map +1 -0
- package/dist/cli/lesson.d.ts.map +1 -1
- package/dist/cli/lesson.js +7 -0
- package/dist/cli/lesson.js.map +1 -1
- package/dist/cli/log.d.ts.map +1 -1
- package/dist/cli/log.js +7 -0
- package/dist/cli/log.js.map +1 -1
- package/dist/cli/package-config.d.ts +12 -0
- package/dist/cli/package-config.d.ts.map +1 -0
- package/dist/cli/package-config.js +37 -0
- package/dist/cli/package-config.js.map +1 -0
- package/dist/cli/package.d.ts +23 -0
- package/dist/cli/package.d.ts.map +1 -0
- package/dist/cli/package.js +285 -0
- package/dist/cli/package.js.map +1 -0
- package/dist/cli/package.test.d.ts +2 -0
- package/dist/cli/package.test.d.ts.map +1 -0
- package/dist/cli/package.test.js +236 -0
- package/dist/cli/package.test.js.map +1 -0
- package/dist/cli/pipeline.d.ts +6 -0
- package/dist/cli/pipeline.d.ts.map +1 -1
- package/dist/cli/pipeline.js +15 -2
- package/dist/cli/pipeline.js.map +1 -1
- package/dist/cli/run/schema.d.ts.map +1 -1
- package/dist/cli/run/schema.js +32 -0
- package/dist/cli/run/schema.js.map +1 -1
- package/dist/cli/run/schema.test.js +51 -0
- package/dist/cli/run/schema.test.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +10 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.d.ts.map +1 -0
- package/dist/cli/skills.js +107 -0
- package/dist/cli/skills.js.map +1 -0
- package/dist/cli/types.d.ts +4 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/update.js +2 -2
- package/package.json +3 -2
- package/src/cli/agents.ts +20 -5
- package/src/cli/artifacts-cli.ts +41 -0
- package/src/cli/baselines.ts +12 -0
- package/src/cli/convoy/artifacts.test.ts +201 -0
- package/src/cli/convoy/artifacts.ts +186 -0
- package/src/cli/convoy/compaction.test.ts +245 -0
- package/src/cli/convoy/compaction.ts +164 -0
- package/src/cli/convoy/contracts.test.ts +279 -0
- package/src/cli/convoy/contracts.ts +280 -0
- package/src/cli/convoy/dag-analysis.test.ts +349 -0
- package/src/cli/convoy/dag-analysis.ts +371 -0
- package/src/cli/convoy/effort-scaling.test.ts +140 -0
- package/src/cli/convoy/effort-scaling.ts +90 -0
- package/src/cli/convoy/engine.test.ts +175 -18
- package/src/cli/convoy/engine.ts +315 -12
- package/src/cli/convoy/event-schemas.ts +55 -0
- package/src/cli/convoy/isolation.test.ts +137 -0
- package/src/cli/convoy/isolation.ts +165 -0
- package/src/cli/convoy/review-stages.test.ts +235 -0
- package/src/cli/convoy/review-stages.ts +166 -0
- package/src/cli/convoy/skill-refinement.test.ts +277 -0
- package/src/cli/convoy/skill-refinement.ts +306 -0
- package/src/cli/convoy/spec-builder.test.ts +61 -0
- package/src/cli/convoy/spec-builder.ts +9 -0
- package/src/cli/convoy/store.test.ts +15 -15
- package/src/cli/convoy/store.ts +26 -4
- package/src/cli/convoy/tdd-gate.test.ts +281 -0
- package/src/cli/convoy/tdd-gate.ts +154 -0
- package/src/cli/convoy/types.ts +51 -0
- package/src/cli/dashboard.ts +55 -0
- package/src/cli/insights.ts +99 -0
- package/src/cli/lesson.ts +8 -0
- package/src/cli/log.ts +8 -0
- package/src/cli/package-config.ts +48 -0
- package/src/cli/package.test.ts +276 -0
- package/src/cli/package.ts +329 -0
- package/src/cli/pipeline.ts +21 -2
- package/src/cli/run/schema.test.ts +58 -0
- package/src/cli/run/schema.ts +33 -0
- package/src/cli/run.ts +14 -1
- package/src/cli/skills.ts +121 -0
- package/src/cli/types.ts +4 -1
- package/src/cli/update.ts +2 -2
- package/src/dashboard/dist/_astro/{index.Je1YjU_y.css → index.BRDFmNzR.css} +1 -1
- package/src/dashboard/dist/index.html +163 -2
- package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
- package/src/dashboard/src/pages/index.astro +162 -1
- package/src/dashboard/src/styles/dashboard.css +85 -0
- package/src/orchestrator/agents/developer.agent.md +8 -0
- package/src/orchestrator/agents/ui-ux-expert.agent.md +7 -0
- package/src/orchestrator/prompts/assess-complexity.prompt.md +13 -0
- package/src/orchestrator/prompts/brainstorm.prompt.md +18 -0
- package/src/orchestrator/prompts/generate-convoy.prompt.md +61 -0
- package/src/orchestrator/skills/decomposition/SKILL.md +35 -0
- package/src/orchestrator/skills/frontend-design/SKILL.md +27 -1
- package/src/orchestrator/skills/project-consistency/SKILL.md +350 -0
|
@@ -99,11 +99,11 @@ describe('DB creation', () => {
|
|
|
99
99
|
expect(row.journal_mode).toBe('wal')
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
it('sets schema version to
|
|
102
|
+
it('sets schema version to 11', () => {
|
|
103
103
|
const db = new DatabaseSync(dbPath)
|
|
104
104
|
const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
|
|
105
105
|
db.close()
|
|
106
|
-
expect(row.user_version).toBe(
|
|
106
|
+
expect(row.user_version).toBe(11)
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
it('creates all required tables', () => {
|
|
@@ -131,7 +131,7 @@ describe('DB creation', () => {
|
|
|
131
131
|
store2.close()
|
|
132
132
|
// Reassign so afterEach does not double-close
|
|
133
133
|
store = createConvoyStore(dbPath)
|
|
134
|
-
expect(row.user_version).toBe(
|
|
134
|
+
expect(row.user_version).toBe(11)
|
|
135
135
|
})
|
|
136
136
|
})
|
|
137
137
|
|
|
@@ -208,8 +208,8 @@ describe('schema migration', () => {
|
|
|
208
208
|
verifyDb.close()
|
|
209
209
|
|
|
210
210
|
expect(cols.map(c => c.name)).toContain('adapter')
|
|
211
|
-
// v1 chains through v2→v3→v4→...→v7→v8→v9→v10 in one init, so final version is
|
|
212
|
-
expect(version.user_version).toBe(
|
|
211
|
+
// v1 chains through v2→v3→v4→...→v7→v8→v9→v10→v11 in one init, so final version is 11
|
|
212
|
+
expect(version.user_version).toBe(11)
|
|
213
213
|
})
|
|
214
214
|
|
|
215
215
|
it('schema migration v2 to v3 adds cost columns', () => {
|
|
@@ -295,7 +295,7 @@ describe('schema migration', () => {
|
|
|
295
295
|
expect(convoyColNames).toContain('total_tokens')
|
|
296
296
|
expect(convoyColNames).toContain('total_cost_usd')
|
|
297
297
|
|
|
298
|
-
expect(version.user_version).toBe(
|
|
298
|
+
expect(version.user_version).toBe(11)
|
|
299
299
|
})
|
|
300
300
|
|
|
301
301
|
it('schema migration v1 to v3 chains correctly in a single init', () => {
|
|
@@ -381,7 +381,7 @@ describe('schema migration', () => {
|
|
|
381
381
|
expect(convoyColNames).toContain('total_tokens')
|
|
382
382
|
expect(convoyColNames).toContain('total_cost_usd')
|
|
383
383
|
|
|
384
|
-
expect(version.user_version).toBe(
|
|
384
|
+
expect(version.user_version).toBe(11)
|
|
385
385
|
})
|
|
386
386
|
|
|
387
387
|
it('schema migration v3 to v4 creates pipeline table and adds pipeline_id to convoy', () => {
|
|
@@ -464,7 +464,7 @@ describe('schema migration', () => {
|
|
|
464
464
|
|
|
465
465
|
expect(convoyCols.map(c => c.name)).toContain('pipeline_id')
|
|
466
466
|
expect(tables.map(t => t.name)).toContain('pipeline')
|
|
467
|
-
expect(version.user_version).toBe(
|
|
467
|
+
expect(version.user_version).toBe(11)
|
|
468
468
|
})
|
|
469
469
|
})
|
|
470
470
|
|
|
@@ -1444,7 +1444,7 @@ describe('schema migration v5 → v6', () => {
|
|
|
1444
1444
|
v5Verify.close()
|
|
1445
1445
|
migratedStore.close()
|
|
1446
1446
|
|
|
1447
|
-
expect(row.user_version).toBe(
|
|
1447
|
+
expect(row.user_version).toBe(11)
|
|
1448
1448
|
expect(taskStepTable?.name).toBe('task_step')
|
|
1449
1449
|
expect(convoy?.id).toBe('convoy-auto')
|
|
1450
1450
|
expect(task?.id).toBe('task-auto')
|
|
@@ -1614,7 +1614,7 @@ describe('schema migration v6→v7 (drift detection columns)', () => {
|
|
|
1614
1614
|
|
|
1615
1615
|
expect(cols.map(c => c.name)).toContain('drift_score')
|
|
1616
1616
|
expect(cols.map(c => c.name)).toContain('drift_retried')
|
|
1617
|
-
expect(version.user_version).toBe(
|
|
1617
|
+
expect(version.user_version).toBe(11)
|
|
1618
1618
|
})
|
|
1619
1619
|
|
|
1620
1620
|
it('new databases include drift_score and drift_retried in CREATE TABLE', () => {
|
|
@@ -1847,9 +1847,9 @@ describe('migration full chain v4→v10', () => {
|
|
|
1847
1847
|
|
|
1848
1848
|
const verifyDb = new DatabaseSync(chainDbPath)
|
|
1849
1849
|
|
|
1850
|
-
// Verify user_version =
|
|
1850
|
+
// Verify user_version = 11
|
|
1851
1851
|
const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
|
|
1852
|
-
expect(version).toBe(
|
|
1852
|
+
expect(version).toBe(11)
|
|
1853
1853
|
|
|
1854
1854
|
// Verify all new tables exist
|
|
1855
1855
|
const tables = (verifyDb.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as Array<{ name: string }>).map(t => t.name)
|
|
@@ -1863,7 +1863,7 @@ describe('migration full chain v4→v10', () => {
|
|
|
1863
1863
|
'gates', 'on_exhausted', 'injected', 'provenance', 'idempotency_key',
|
|
1864
1864
|
'current_step', 'total_steps', 'review_level', 'review_verdict', 'review_tokens',
|
|
1865
1865
|
'review_model', 'panel_attempts', 'dispute_id', 'drift_score', 'drift_retried',
|
|
1866
|
-
'outputs', 'inputs', 'discovered_issues',
|
|
1866
|
+
'outputs', 'inputs', 'discovered_issues', 'compaction_count',
|
|
1867
1867
|
]) {
|
|
1868
1868
|
expect(taskCols).toContain(col)
|
|
1869
1869
|
}
|
|
@@ -2525,10 +2525,10 @@ describe('v9→v10 migration', () => {
|
|
|
2525
2525
|
|
|
2526
2526
|
migratedStore.close()
|
|
2527
2527
|
|
|
2528
|
-
// Verify version =
|
|
2528
|
+
// Verify version = 11
|
|
2529
2529
|
const verifyDb = new DatabaseSync(migDb)
|
|
2530
2530
|
const version = (verifyDb.prepare('PRAGMA user_version').get() as { user_version: number }).user_version
|
|
2531
|
-
expect(version).toBe(
|
|
2531
|
+
expect(version).toBe(11)
|
|
2532
2532
|
|
|
2533
2533
|
// Verify new REAL columns exist
|
|
2534
2534
|
const convoyCols = (verifyDb.prepare('PRAGMA table_info(convoy)').all() as Array<{ name: string }>).map(c => c.name)
|
package/src/cli/convoy/store.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
TaskStepRecord,
|
|
18
18
|
} from './types.js'
|
|
19
19
|
|
|
20
|
-
const SCHEMA_VERSION =
|
|
20
|
+
const SCHEMA_VERSION = 11
|
|
21
21
|
|
|
22
22
|
// ── Size limits (bytes) ────────────────────────────────────────────────────────
|
|
23
23
|
const LIMIT_SPEC_YAML = 256 * 1024 // 256 KB
|
|
@@ -83,7 +83,7 @@ export interface ConvoyStore {
|
|
|
83
83
|
| 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
|
|
84
84
|
| 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
|
|
85
85
|
| 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
|
|
86
|
-
| 'drift_score' | 'drift_retried' | 'discovered_issues'
|
|
86
|
+
| 'drift_score' | 'drift_retried' | 'discovered_issues' | 'compaction_count'
|
|
87
87
|
> & { outputs?: string | null; inputs?: string | null },
|
|
88
88
|
): void
|
|
89
89
|
insertInjectedTask(record: TaskRecord): void
|
|
@@ -101,6 +101,7 @@ export interface ConvoyStore {
|
|
|
101
101
|
TaskRecord,
|
|
102
102
|
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
103
103
|
| 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
|
|
104
|
+
| 'contract_result'
|
|
104
105
|
>
|
|
105
106
|
>,
|
|
106
107
|
): void
|
|
@@ -114,6 +115,7 @@ export interface ConvoyStore {
|
|
|
114
115
|
convoyId: string,
|
|
115
116
|
fields: Partial<Pick<TaskRecord, 'drift_score' | 'drift_retried'>>,
|
|
116
117
|
): void
|
|
118
|
+
updateTaskCompaction(taskId: string, convoyId: string, compactionCount: number): void
|
|
117
119
|
updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void
|
|
118
120
|
getReadyTasks(convoyId: string): TaskRecord[]
|
|
119
121
|
insertTaskStep(record: Omit<TaskStepRecord, 'id'>): number
|
|
@@ -259,9 +261,11 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
259
261
|
dispute_id TEXT,
|
|
260
262
|
drift_score REAL,
|
|
261
263
|
drift_retried INTEGER NOT NULL DEFAULT 0,
|
|
264
|
+
compaction_count INTEGER NOT NULL DEFAULT 0,
|
|
262
265
|
outputs TEXT,
|
|
263
266
|
inputs TEXT,
|
|
264
|
-
discovered_issues TEXT
|
|
267
|
+
discovered_issues TEXT,
|
|
268
|
+
contract_result TEXT
|
|
265
269
|
);
|
|
266
270
|
|
|
267
271
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_task_idempotency ON task(convoy_id, idempotency_key)
|
|
@@ -416,6 +420,10 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
416
420
|
migrateSchema(this.db, this.dbPath, 9, 10)
|
|
417
421
|
version = 10
|
|
418
422
|
}
|
|
423
|
+
if (version === 10) {
|
|
424
|
+
migrateSchema(this.db, this.dbPath, 10, 11)
|
|
425
|
+
version = 11
|
|
426
|
+
}
|
|
419
427
|
}
|
|
420
428
|
|
|
421
429
|
insertConvoy(
|
|
@@ -504,7 +512,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
504
512
|
| 'on_exhausted' | 'injected' | 'provenance' | 'idempotency_key'
|
|
505
513
|
| 'current_step' | 'total_steps' | 'review_level' | 'review_verdict'
|
|
506
514
|
| 'review_tokens' | 'review_model' | 'panel_attempts' | 'dispute_id'
|
|
507
|
-
| 'drift_score' | 'drift_retried' | 'discovered_issues'
|
|
515
|
+
| 'drift_score' | 'drift_retried' | 'discovered_issues' | 'compaction_count'
|
|
508
516
|
> & { outputs?: string | null; inputs?: string | null },
|
|
509
517
|
): void {
|
|
510
518
|
this.db
|
|
@@ -592,6 +600,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
592
600
|
TaskRecord,
|
|
593
601
|
| 'worker_id' | 'worktree' | 'output' | 'exit_code' | 'started_at' | 'finished_at'
|
|
594
602
|
| 'retries' | 'prompt_tokens' | 'completion_tokens' | 'total_tokens' | 'cost_usd' | 'prompt'
|
|
603
|
+
| 'contract_result'
|
|
595
604
|
>
|
|
596
605
|
>,
|
|
597
606
|
): void {
|
|
@@ -603,6 +612,7 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
603
612
|
const extraFields = [
|
|
604
613
|
'worker_id', 'worktree', 'output', 'exit_code', 'started_at', 'finished_at',
|
|
605
614
|
'retries', 'prompt_tokens', 'completion_tokens', 'total_tokens', 'cost_usd', 'prompt',
|
|
615
|
+
'contract_result',
|
|
606
616
|
] as const
|
|
607
617
|
|
|
608
618
|
if (extra) {
|
|
@@ -708,6 +718,12 @@ class ConvoyStoreImpl implements ConvoyStore {
|
|
|
708
718
|
this.db.prepare(`UPDATE task SET ${sets.join(', ')} WHERE id = :id AND convoy_id = :convoy_id`).run(params)
|
|
709
719
|
}
|
|
710
720
|
|
|
721
|
+
updateTaskCompaction(taskId: string, convoyId: string, compactionCount: number): void {
|
|
722
|
+
this.db
|
|
723
|
+
.prepare('UPDATE task SET compaction_count = :compaction_count WHERE id = :id AND convoy_id = :convoy_id')
|
|
724
|
+
.run({ id: taskId, convoy_id: convoyId, compaction_count: compactionCount })
|
|
725
|
+
}
|
|
726
|
+
|
|
711
727
|
updateTaskDisputeStatus(taskId: string, convoyId: string, status: ConvoyTaskStatus, disputeId: string): void {
|
|
712
728
|
this.db
|
|
713
729
|
.prepare(
|
|
@@ -1334,6 +1350,12 @@ export function migrateSchema(db: DatabaseSync, dbPath: string, fromVersion: num
|
|
|
1334
1350
|
UPDATE pipeline SET total_cost_usd_num = CAST(total_cost_usd AS REAL) WHERE total_cost_usd IS NOT NULL;
|
|
1335
1351
|
`)
|
|
1336
1352
|
}
|
|
1353
|
+
if (v === 10) {
|
|
1354
|
+
db.exec(`
|
|
1355
|
+
ALTER TABLE task ADD COLUMN contract_result TEXT;
|
|
1356
|
+
ALTER TABLE task ADD COLUMN compaction_count INTEGER NOT NULL DEFAULT 0;
|
|
1357
|
+
`)
|
|
1358
|
+
}
|
|
1337
1359
|
db.exec('COMMIT')
|
|
1338
1360
|
} catch (err) {
|
|
1339
1361
|
try { db.exec('ROLLBACK') } catch { /* ignore */ }
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
checkTDD,
|
|
4
|
+
formatTDDFailure,
|
|
5
|
+
DEFAULT_TDD_CONFIG,
|
|
6
|
+
type TDDCheckResult,
|
|
7
|
+
} from './tdd-gate.js'
|
|
8
|
+
import type { TDDGateConfig } from './types.js'
|
|
9
|
+
|
|
10
|
+
const BASE_CONFIG: TDDGateConfig = { ...DEFAULT_TDD_CONFIG }
|
|
11
|
+
|
|
12
|
+
describe('checkTDD', () => {
|
|
13
|
+
describe('disabled / skipped', () => {
|
|
14
|
+
it('returns skipped when config.enabled is false', () => {
|
|
15
|
+
const result = checkTDD(['src/foo.ts'], [], { ...BASE_CONFIG, enabled: false })
|
|
16
|
+
expect(result.skipped).toBe(true)
|
|
17
|
+
expect(result.skip_reason).toBe('disabled')
|
|
18
|
+
expect(result.passed).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns skipped for exempt agent', () => {
|
|
22
|
+
const result = checkTDD(['src/foo.ts'], [], BASE_CONFIG, 'documentation-writer')
|
|
23
|
+
expect(result.skipped).toBe(true)
|
|
24
|
+
expect(result.skip_reason).toBe('exempt_agent')
|
|
25
|
+
expect(result.passed).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns skipped for all exempt agents', () => {
|
|
29
|
+
for (const agent of ['documentation-writer', 'copywriter', 'seo-specialist', 'researcher']) {
|
|
30
|
+
const result = checkTDD(['src/foo.ts'], [], BASE_CONFIG, agent)
|
|
31
|
+
expect(result.skipped).toBe(true)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does NOT skip for unknown agent', () => {
|
|
36
|
+
const result = checkTDD(['src/foo.ts', 'src/foo.test.ts'], [], BASE_CONFIG, 'developer')
|
|
37
|
+
expect(result.skipped).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('exclude patterns', () => {
|
|
42
|
+
it('excludes **/types.ts', () => {
|
|
43
|
+
const result = checkTDD(['src/cli/convoy/types.ts'], [], BASE_CONFIG)
|
|
44
|
+
expect(result.excluded_files).toContain('src/cli/convoy/types.ts')
|
|
45
|
+
expect(result.new_source_files).toHaveLength(0)
|
|
46
|
+
expect(result.passed).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('excludes **/index.ts', () => {
|
|
50
|
+
const result = checkTDD(['src/index.ts'], [], BASE_CONFIG)
|
|
51
|
+
expect(result.excluded_files).toContain('src/index.ts')
|
|
52
|
+
expect(result.passed).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('excludes **/*.d.ts', () => {
|
|
56
|
+
const result = checkTDD(['src/foo.d.ts'], [], BASE_CONFIG)
|
|
57
|
+
expect(result.excluded_files).toContain('src/foo.d.ts')
|
|
58
|
+
expect(result.passed).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('excludes **/constants.ts', () => {
|
|
62
|
+
const result = checkTDD(['src/constants.ts'], [], BASE_CONFIG)
|
|
63
|
+
expect(result.excluded_files).toContain('src/constants.ts')
|
|
64
|
+
expect(result.passed).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('excludes **/schemas.ts', () => {
|
|
68
|
+
const result = checkTDD(['src/schemas.ts'], [], BASE_CONFIG)
|
|
69
|
+
expect(result.excluded_files).toContain('src/schemas.ts')
|
|
70
|
+
expect(result.passed).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('test file self-exclusion', () => {
|
|
75
|
+
it('excludes .test.ts files from source candidates', () => {
|
|
76
|
+
const result = checkTDD(['src/foo.test.ts'], [], BASE_CONFIG)
|
|
77
|
+
expect(result.excluded_files).toContain('src/foo.test.ts')
|
|
78
|
+
expect(result.new_source_files).toHaveLength(0)
|
|
79
|
+
expect(result.passed).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('excludes .spec.ts files from source candidates', () => {
|
|
83
|
+
const result = checkTDD(['src/foo.spec.ts'], [], BASE_CONFIG)
|
|
84
|
+
expect(result.excluded_files).toContain('src/foo.spec.ts')
|
|
85
|
+
expect(result.passed).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('block mode (default)', () => {
|
|
90
|
+
it('passes when source file has test in changedFiles', () => {
|
|
91
|
+
const result = checkTDD(['src/foo.ts', 'src/foo.test.ts'], [], BASE_CONFIG)
|
|
92
|
+
expect(result.passed).toBe(true)
|
|
93
|
+
expect(result.missing_test_files).toHaveLength(0)
|
|
94
|
+
expect(result.existing_test_files).toContain('src/foo.ts')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('blocks when source file has no test', () => {
|
|
98
|
+
const result = checkTDD(['src/foo.ts'], [], BASE_CONFIG)
|
|
99
|
+
expect(result.passed).toBe(false)
|
|
100
|
+
expect(result.missing_test_files).toContain('src/foo.ts')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('passes when test already exists in allFiles (not changedFiles)', () => {
|
|
104
|
+
const result = checkTDD(['src/foo.ts'], ['src/foo.test.ts'], BASE_CONFIG)
|
|
105
|
+
expect(result.passed).toBe(true)
|
|
106
|
+
expect(result.existing_test_files).toContain('src/foo.ts')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('recognizes .spec.ts as valid test file', () => {
|
|
110
|
+
const result = checkTDD(['src/foo.ts', 'src/foo.spec.ts'], [], BASE_CONFIG)
|
|
111
|
+
expect(result.passed).toBe(true)
|
|
112
|
+
expect(result.existing_test_files).toContain('src/foo.ts')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('handles multiple files, some with tests and some without', () => {
|
|
116
|
+
const result = checkTDD(
|
|
117
|
+
['src/a.ts', 'src/b.ts', 'src/a.test.ts'],
|
|
118
|
+
[],
|
|
119
|
+
BASE_CONFIG,
|
|
120
|
+
)
|
|
121
|
+
expect(result.passed).toBe(false)
|
|
122
|
+
expect(result.missing_test_files).toContain('src/b.ts')
|
|
123
|
+
expect(result.existing_test_files).toContain('src/a.ts')
|
|
124
|
+
expect(result.missing_test_files).not.toContain('src/a.ts')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('passes when all source files have tests', () => {
|
|
128
|
+
const result = checkTDD(
|
|
129
|
+
['src/a.ts', 'src/b.ts', 'src/a.test.ts', 'src/b.test.ts'],
|
|
130
|
+
[],
|
|
131
|
+
BASE_CONFIG,
|
|
132
|
+
)
|
|
133
|
+
expect(result.passed).toBe(true)
|
|
134
|
+
expect(result.missing_test_files).toHaveLength(0)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('warn mode', () => {
|
|
139
|
+
const warnConfig: TDDGateConfig = { ...BASE_CONFIG, mode: 'warn' }
|
|
140
|
+
|
|
141
|
+
it('passes even when test file is missing', () => {
|
|
142
|
+
const result = checkTDD(['src/foo.ts'], [], warnConfig)
|
|
143
|
+
expect(result.passed).toBe(true)
|
|
144
|
+
expect(result.missing_test_files).toContain('src/foo.ts')
|
|
145
|
+
expect(result.skipped).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('still reports all missing test files', () => {
|
|
149
|
+
const result = checkTDD(['src/a.ts', 'src/b.ts'], [], warnConfig)
|
|
150
|
+
expect(result.passed).toBe(true)
|
|
151
|
+
expect(result.missing_test_files).toHaveLength(2)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('source pattern filtering', () => {
|
|
156
|
+
it('ignores files not matching source_patterns', () => {
|
|
157
|
+
const result = checkTDD(['README.md', 'package.json', '.gitignore'], [], BASE_CONFIG)
|
|
158
|
+
expect(result.new_source_files).toHaveLength(0)
|
|
159
|
+
expect(result.passed).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('custom source_patterns work', () => {
|
|
163
|
+
const config: TDDGateConfig = { ...BASE_CONFIG, source_patterns: ['lib/**/*.ts'] }
|
|
164
|
+
const result = checkTDD(['lib/foo.ts'], [], config)
|
|
165
|
+
expect(result.passed).toBe(false)
|
|
166
|
+
expect(result.missing_test_files).toContain('lib/foo.ts')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('nested file paths', () => {
|
|
171
|
+
it('resolves test path correctly for deeply nested source file', () => {
|
|
172
|
+
const result = checkTDD(
|
|
173
|
+
['src/cli/convoy/foo.ts', 'src/cli/convoy/foo.test.ts'],
|
|
174
|
+
[],
|
|
175
|
+
BASE_CONFIG,
|
|
176
|
+
)
|
|
177
|
+
expect(result.passed).toBe(true)
|
|
178
|
+
expect(result.existing_test_files).toContain('src/cli/convoy/foo.ts')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('resolves test path against allFiles for pre-existing test', () => {
|
|
182
|
+
const result = checkTDD(
|
|
183
|
+
['src/cli/convoy/compaction.ts'],
|
|
184
|
+
['src/cli/convoy/compaction.test.ts'],
|
|
185
|
+
BASE_CONFIG,
|
|
186
|
+
)
|
|
187
|
+
expect(result.passed).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('blocks when nested source file has no test', () => {
|
|
191
|
+
const result = checkTDD(['src/cli/convoy/artifacts.ts'], [], BASE_CONFIG)
|
|
192
|
+
expect(result.passed).toBe(false)
|
|
193
|
+
expect(result.missing_test_files).toContain('src/cli/convoy/artifacts.ts')
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('empty inputs', () => {
|
|
198
|
+
it('passes with no changed files', () => {
|
|
199
|
+
const result = checkTDD([], [], BASE_CONFIG)
|
|
200
|
+
expect(result.passed).toBe(true)
|
|
201
|
+
expect(result.new_source_files).toHaveLength(0)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('passes with no changed files even with allFiles', () => {
|
|
205
|
+
const result = checkTDD([], ['src/foo.ts', 'src/foo.test.ts'], BASE_CONFIG)
|
|
206
|
+
expect(result.passed).toBe(true)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe('formatTDDFailure', () => {
|
|
212
|
+
it('returns success message when no missing test files', () => {
|
|
213
|
+
const result: TDDCheckResult = {
|
|
214
|
+
passed: true,
|
|
215
|
+
new_source_files: ['src/foo.ts'],
|
|
216
|
+
missing_test_files: [],
|
|
217
|
+
existing_test_files: ['src/foo.ts'],
|
|
218
|
+
excluded_files: [],
|
|
219
|
+
skipped: false,
|
|
220
|
+
}
|
|
221
|
+
expect(formatTDDFailure(result)).toBe('TDD Gate: all source files have corresponding tests.')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('formats missing file with correct test path', () => {
|
|
225
|
+
const result: TDDCheckResult = {
|
|
226
|
+
passed: false,
|
|
227
|
+
new_source_files: ['src/cli/convoy/artifacts.ts'],
|
|
228
|
+
missing_test_files: ['src/cli/convoy/artifacts.ts'],
|
|
229
|
+
existing_test_files: [],
|
|
230
|
+
excluded_files: [],
|
|
231
|
+
skipped: false,
|
|
232
|
+
}
|
|
233
|
+
const output = formatTDDFailure(result)
|
|
234
|
+
expect(output).toContain('TDD Gate BLOCKED: New source files without tests:')
|
|
235
|
+
expect(output).toContain('src/cli/convoy/artifacts.ts → missing src/cli/convoy/artifacts.test.ts')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('formats multiple missing files', () => {
|
|
239
|
+
const result: TDDCheckResult = {
|
|
240
|
+
passed: false,
|
|
241
|
+
new_source_files: ['src/cli/convoy/artifacts.ts', 'src/cli/convoy/compaction.ts'],
|
|
242
|
+
missing_test_files: ['src/cli/convoy/artifacts.ts', 'src/cli/convoy/compaction.ts'],
|
|
243
|
+
existing_test_files: [],
|
|
244
|
+
excluded_files: [],
|
|
245
|
+
skipped: false,
|
|
246
|
+
}
|
|
247
|
+
const output = formatTDDFailure(result)
|
|
248
|
+
expect(output).toContain('src/cli/convoy/artifacts.ts → missing src/cli/convoy/artifacts.test.ts')
|
|
249
|
+
expect(output).toContain('src/cli/convoy/compaction.ts → missing src/cli/convoy/compaction.test.ts')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('formats file without directory correctly', () => {
|
|
253
|
+
const result: TDDCheckResult = {
|
|
254
|
+
passed: false,
|
|
255
|
+
new_source_files: ['foo.ts'],
|
|
256
|
+
missing_test_files: ['foo.ts'],
|
|
257
|
+
existing_test_files: [],
|
|
258
|
+
excluded_files: [],
|
|
259
|
+
skipped: false,
|
|
260
|
+
}
|
|
261
|
+
const output = formatTDDFailure(result)
|
|
262
|
+
expect(output).toContain('foo.ts → missing foo.test.ts')
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
describe('DEFAULT_TDD_CONFIG', () => {
|
|
267
|
+
it('has expected defaults', () => {
|
|
268
|
+
expect(DEFAULT_TDD_CONFIG.enabled).toBe(true)
|
|
269
|
+
expect(DEFAULT_TDD_CONFIG.mode).toBe('block')
|
|
270
|
+
expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('documentation-writer')
|
|
271
|
+
expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('copywriter')
|
|
272
|
+
expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('seo-specialist')
|
|
273
|
+
expect(DEFAULT_TDD_CONFIG.exempt_agents).toContain('researcher')
|
|
274
|
+
expect(DEFAULT_TDD_CONFIG.source_patterns).toContain('src/**/*.ts')
|
|
275
|
+
expect(DEFAULT_TDD_CONFIG.test_patterns).toContain('{name}.test.ts')
|
|
276
|
+
expect(DEFAULT_TDD_CONFIG.test_patterns).toContain('{name}.spec.ts')
|
|
277
|
+
expect(DEFAULT_TDD_CONFIG.exclude_patterns).toContain('**/types.ts')
|
|
278
|
+
expect(DEFAULT_TDD_CONFIG.exclude_patterns).toContain('**/index.ts')
|
|
279
|
+
expect(DEFAULT_TDD_CONFIG.exclude_patterns).toContain('**/*.d.ts')
|
|
280
|
+
})
|
|
281
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { TDDGateConfig } from './types.js'
|
|
2
|
+
|
|
3
|
+
export type { TDDGateConfig }
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_TDD_CONFIG: TDDGateConfig = {
|
|
6
|
+
enabled: true,
|
|
7
|
+
source_patterns: ['src/**/*.ts'],
|
|
8
|
+
test_patterns: ['{name}.test.ts', '{name}.spec.ts'],
|
|
9
|
+
exclude_patterns: [
|
|
10
|
+
'**/types.ts',
|
|
11
|
+
'**/index.ts',
|
|
12
|
+
'**/*.d.ts',
|
|
13
|
+
'**/constants.ts',
|
|
14
|
+
'**/schemas.ts',
|
|
15
|
+
],
|
|
16
|
+
mode: 'block',
|
|
17
|
+
exempt_agents: ['documentation-writer', 'copywriter', 'seo-specialist', 'researcher'],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TDDCheckResult {
|
|
21
|
+
passed: boolean
|
|
22
|
+
new_source_files: string[]
|
|
23
|
+
missing_test_files: string[]
|
|
24
|
+
existing_test_files: string[]
|
|
25
|
+
excluded_files: string[]
|
|
26
|
+
skipped: boolean
|
|
27
|
+
skip_reason?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── Glob matching ─────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function matchGlob(pattern: string, filePath: string): boolean {
|
|
33
|
+
const path = filePath.replace(/\\/g, '/')
|
|
34
|
+
const regexStr = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
36
|
+
.replace(/\*\*\//g, '(?:.+/)?')
|
|
37
|
+
.replace(/\*\*/g, '.*')
|
|
38
|
+
.replace(/\*/g, '[^/]*')
|
|
39
|
+
return new RegExp('^' + regexStr + '$').test(path)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isTestFile(filePath: string, testPatterns: string[]): boolean {
|
|
43
|
+
const basename = filePath.split('/').pop() ?? ''
|
|
44
|
+
return testPatterns.some(pattern => {
|
|
45
|
+
const idx = pattern.indexOf('{name}')
|
|
46
|
+
if (idx < 0) return false
|
|
47
|
+
const prefix = pattern.slice(0, idx).replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
48
|
+
const suffix = pattern.slice(idx + '{name}'.length).replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
49
|
+
return new RegExp('^' + prefix + '[^/]+' + suffix + '$').test(basename)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveTestPaths(sourceFile: string, testPatterns: string[]): string[] {
|
|
54
|
+
const lastSlash = sourceFile.lastIndexOf('/')
|
|
55
|
+
const dir = lastSlash >= 0 ? sourceFile.slice(0, lastSlash) : ''
|
|
56
|
+
const basename = lastSlash >= 0 ? sourceFile.slice(lastSlash + 1) : sourceFile
|
|
57
|
+
const lastDot = basename.lastIndexOf('.')
|
|
58
|
+
const nameWithoutExt = lastDot >= 0 ? basename.slice(0, lastDot) : basename
|
|
59
|
+
|
|
60
|
+
return testPatterns.map(pattern => {
|
|
61
|
+
const testBasename = pattern.replace('{name}', nameWithoutExt)
|
|
62
|
+
return dir ? dir + '/' + testBasename : testBasename
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export function checkTDD(
|
|
69
|
+
changedFiles: string[],
|
|
70
|
+
allFiles: string[],
|
|
71
|
+
config: TDDGateConfig,
|
|
72
|
+
agent?: string,
|
|
73
|
+
): TDDCheckResult {
|
|
74
|
+
const empty: TDDCheckResult = {
|
|
75
|
+
passed: true,
|
|
76
|
+
new_source_files: [],
|
|
77
|
+
missing_test_files: [],
|
|
78
|
+
existing_test_files: [],
|
|
79
|
+
excluded_files: [],
|
|
80
|
+
skipped: false,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!config.enabled) {
|
|
84
|
+
return { ...empty, skipped: true, skip_reason: 'disabled' }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (agent !== undefined && config.exempt_agents.includes(agent)) {
|
|
88
|
+
return { ...empty, skipped: true, skip_reason: 'exempt_agent' }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const allFileSet = new Set([...changedFiles, ...allFiles])
|
|
92
|
+
|
|
93
|
+
// Filter to files matching source_patterns
|
|
94
|
+
const sourceFiles = changedFiles.filter(f =>
|
|
95
|
+
config.source_patterns.some(p => matchGlob(p, f)),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// Separate excluded vs candidate files
|
|
99
|
+
const excluded: string[] = []
|
|
100
|
+
const candidates: string[] = []
|
|
101
|
+
for (const f of sourceFiles) {
|
|
102
|
+
if (config.exclude_patterns.some(p => matchGlob(p, f))) {
|
|
103
|
+
excluded.push(f)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (isTestFile(f, config.test_patterns)) {
|
|
107
|
+
excluded.push(f)
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
candidates.push(f)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// For each candidate, check for corresponding test
|
|
114
|
+
const missingTestFiles: string[] = []
|
|
115
|
+
const existingTestFiles: string[] = []
|
|
116
|
+
|
|
117
|
+
for (const sourceFile of candidates) {
|
|
118
|
+
const testPaths = resolveTestPaths(sourceFile, config.test_patterns)
|
|
119
|
+
const hasTest = testPaths.some(tp => allFileSet.has(tp))
|
|
120
|
+
if (hasTest) {
|
|
121
|
+
existingTestFiles.push(sourceFile)
|
|
122
|
+
} else {
|
|
123
|
+
missingTestFiles.push(sourceFile)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const passed = missingTestFiles.length === 0 || config.mode === 'warn'
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
passed,
|
|
131
|
+
new_source_files: candidates,
|
|
132
|
+
missing_test_files: missingTestFiles,
|
|
133
|
+
existing_test_files: existingTestFiles,
|
|
134
|
+
excluded_files: excluded,
|
|
135
|
+
skipped: false,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function formatTDDFailure(result: TDDCheckResult): string {
|
|
140
|
+
if (result.missing_test_files.length === 0) {
|
|
141
|
+
return 'TDD Gate: all source files have corresponding tests.'
|
|
142
|
+
}
|
|
143
|
+
const lines = ['TDD Gate BLOCKED: New source files without tests:']
|
|
144
|
+
for (const sourceFile of result.missing_test_files) {
|
|
145
|
+
const lastSlash = sourceFile.lastIndexOf('/')
|
|
146
|
+
const dir = lastSlash >= 0 ? sourceFile.slice(0, lastSlash) : ''
|
|
147
|
+
const basename = lastSlash >= 0 ? sourceFile.slice(lastSlash + 1) : sourceFile
|
|
148
|
+
const lastDot = basename.lastIndexOf('.')
|
|
149
|
+
const nameWithoutExt = lastDot >= 0 ? basename.slice(0, lastDot) : basename
|
|
150
|
+
const testFile = (dir ? dir + '/' : '') + nameWithoutExt + '.test.ts'
|
|
151
|
+
lines.push(` - ${sourceFile} → missing ${testFile}`)
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|