@cleocode/contracts 2026.5.112 → 2026.5.114
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/dist/__tests__/cli-meta-suggested-next.test.d.ts +21 -0
- package/dist/__tests__/cli-meta-suggested-next.test.d.ts.map +1 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js +70 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js.map +1 -0
- package/dist/config/__tests__/manifest.test.d.ts +13 -0
- package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/config/__tests__/manifest.test.js +143 -0
- package/dist/config/__tests__/manifest.test.js.map +1 -0
- package/dist/config/manifest.d.ts +158 -0
- package/dist/config/manifest.d.ts.map +1 -0
- package/dist/config/manifest.js +123 -0
- package/dist/config/manifest.js.map +1 -0
- package/dist/docs/__tests__/provenance.test.d.ts +14 -0
- package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
- package/dist/docs/__tests__/provenance.test.js +579 -0
- package/dist/docs/__tests__/provenance.test.js.map +1 -0
- package/dist/docs/provenance.d.ts +731 -0
- package/dist/docs/provenance.d.ts.map +1 -0
- package/dist/docs/provenance.js +258 -0
- package/dist/docs/provenance.js.map +1 -0
- package/dist/docs-taxonomy.d.ts +24 -0
- package/dist/docs-taxonomy.d.ts.map +1 -1
- package/dist/docs-taxonomy.js +27 -0
- package/dist/docs-taxonomy.js.map +1 -1
- package/dist/doctor.d.ts +101 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/exit-codes.d.ts +37 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +37 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
- package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
- package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
- package/dist/invariants/adr-056-release.d.ts +42 -0
- package/dist/invariants/adr-056-release.d.ts.map +1 -0
- package/dist/invariants/adr-056-release.js +143 -0
- package/dist/invariants/adr-056-release.js.map +1 -0
- package/dist/invariants/adr-070-orchestration.d.ts +73 -0
- package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
- package/dist/invariants/adr-070-orchestration.js +290 -0
- package/dist/invariants/adr-070-orchestration.js.map +1 -0
- package/dist/invariants/index.d.ts +4 -1
- package/dist/invariants/index.d.ts.map +1 -1
- package/dist/invariants/index.js +11 -2
- package/dist/invariants/index.js.map +1 -1
- package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
- package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
- package/dist/operations/__tests__/input-contract.test.js +158 -0
- package/dist/operations/__tests__/input-contract.test.js.map +1 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.d.ts +18 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.d.ts.map +1 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js +80 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js.map +1 -0
- package/dist/operations/docs.d.ts +16 -0
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/input-contract.d.ts +267 -0
- package/dist/operations/input-contract.d.ts.map +1 -0
- package/dist/operations/input-contract.js +29 -0
- package/dist/operations/input-contract.js.map +1 -0
- package/dist/operations/tasks.d.ts +96 -0
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js +237 -1
- package/dist/operations/tasks.js.map +1 -1
- package/dist/templates/__tests__/manifest.test.d.ts +18 -0
- package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/templates/__tests__/manifest.test.js +123 -0
- package/dist/templates/__tests__/manifest.test.js.map +1 -0
- package/dist/templates/manifest.d.ts +186 -0
- package/dist/templates/manifest.d.ts.map +1 -0
- package/dist/templates/manifest.js +165 -0
- package/dist/templates/manifest.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
- package/src/config/__tests__/manifest.test.ts +176 -0
- package/src/config/manifest.ts +218 -0
- package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
- package/src/docs/__tests__/provenance.test.ts +648 -0
- package/src/docs/provenance.ts +601 -0
- package/src/docs-taxonomy.ts +55 -0
- package/src/doctor.ts +111 -0
- package/src/errors.ts +4 -0
- package/src/exit-codes.ts +39 -0
- package/src/index.ts +102 -0
- package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
- package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
- package/src/invariants/adr-056-release.ts +154 -0
- package/src/invariants/adr-070-orchestration.ts +317 -0
- package/src/invariants/index.ts +11 -2
- package/src/operations/__tests__/input-contract.test.ts +188 -0
- package/src/operations/__tests__/tasks-input-contracts.test.ts +106 -0
- package/src/operations/docs.ts +16 -0
- package/src/operations/input-contract.ts +301 -0
- package/src/operations/tasks.ts +308 -0
- package/src/templates/__tests__/manifest.test.ts +140 -0
- package/src/templates/manifest.ts +208 -0
package/src/operations/tasks.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type {
|
|
|
36
36
|
TaskView,
|
|
37
37
|
} from '../tasks.js';
|
|
38
38
|
import type { DocsType } from './docs.js';
|
|
39
|
+
import type { JsonSchema, OperationInputContract } from './input-contract.js';
|
|
39
40
|
|
|
40
41
|
export type { TaskPriority, TaskStatus };
|
|
41
42
|
|
|
@@ -1352,3 +1353,310 @@ export type TasksOps = {
|
|
|
1352
1353
|
/** T10121 — idempotent cron-safe auto-close repair (supersedes T10098 scope). */
|
|
1353
1354
|
readonly 'saga.reconcile': readonly [TasksSagaReconcileParams, TasksSagaReconcileResult];
|
|
1354
1355
|
};
|
|
1356
|
+
|
|
1357
|
+
// ---------------------------------------------------------------------------
|
|
1358
|
+
// OperationInputContract schemas (T9917 / Epic T9903 / Saga T9855)
|
|
1359
|
+
// ---------------------------------------------------------------------------
|
|
1360
|
+
//
|
|
1361
|
+
// JSON Schema documents that describe the accepted input shape for each
|
|
1362
|
+
// tasks.* mutate operation. The `tasks.add`, `tasks.add-batch`, and
|
|
1363
|
+
// `tasks.update` schemas back the schema-first `mutate(operation, input)`
|
|
1364
|
+
// DX surface introduced by T9914 and validated by T9915.
|
|
1365
|
+
//
|
|
1366
|
+
// Schemas live in `contracts/` because the CLI (`packages/cleo/`) and the
|
|
1367
|
+
// CORE registry (`packages/core/`) both depend on them — contracts is the
|
|
1368
|
+
// leaf package both packages already import from, so colocation avoids a
|
|
1369
|
+
// fan-out violation (T10074).
|
|
1370
|
+
//
|
|
1371
|
+
// Each schema sets `additionalProperties: false` to reject typo'd flags
|
|
1372
|
+
// outright instead of silently dropping them on the floor at the dispatch
|
|
1373
|
+
// boundary.
|
|
1374
|
+
// ---------------------------------------------------------------------------
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Per-task entry inside a `tasks.add-batch` payload.
|
|
1378
|
+
*
|
|
1379
|
+
* Mirrors the shape of {@link TasksAddBatchParams.tasks | TasksAddBatchParams.tasks[]}
|
|
1380
|
+
* exactly — every field is identical to a `tasks.add` spec EXCEPT `dryRun`,
|
|
1381
|
+
* which is hoisted to the batch level so a single dispatch can preview the
|
|
1382
|
+
* whole batch atomically.
|
|
1383
|
+
*
|
|
1384
|
+
* @task T9917
|
|
1385
|
+
*/
|
|
1386
|
+
export interface TasksAddBatchEntry {
|
|
1387
|
+
/** Task title (3–500 characters). Required. */
|
|
1388
|
+
title: string;
|
|
1389
|
+
/** Detailed task description (must differ meaningfully from title). */
|
|
1390
|
+
description?: string;
|
|
1391
|
+
/** Parent task ID (makes this task a subtask). */
|
|
1392
|
+
parent?: string;
|
|
1393
|
+
/** Comma-separated dependency task IDs. */
|
|
1394
|
+
depends?: string[];
|
|
1395
|
+
/** Task priority (low | medium | high | critical). */
|
|
1396
|
+
priority?: string;
|
|
1397
|
+
/** Lowercase alphanumeric + hyphens + periods labels. */
|
|
1398
|
+
labels?: string[];
|
|
1399
|
+
/** Task type (saga | epic | task | subtask). */
|
|
1400
|
+
type?: TaskType; // SSoT-EXEMPT:kind≠type — same axis split as TasksAddBatchParams (T944) // ssot-exempt-ok: mirrors parent type
|
|
1401
|
+
/** Pipe-separated acceptance criteria, normalized to string[]. */
|
|
1402
|
+
acceptance?: string[];
|
|
1403
|
+
/** Phase slug to assign the task to. */
|
|
1404
|
+
phase?: string;
|
|
1405
|
+
/** Scope size estimate (small | medium | large). */
|
|
1406
|
+
size?: string;
|
|
1407
|
+
/** Initial note entry for the task. */
|
|
1408
|
+
notes?: string;
|
|
1409
|
+
/** Files touched by the task (relative paths). */
|
|
1410
|
+
files?: string[];
|
|
1411
|
+
/** Task kind / intent axis (work | research | experiment | bug | spike | release). */
|
|
1412
|
+
kind?: string;
|
|
1413
|
+
/** Task scope / granularity axis (project | feature | unit). */
|
|
1414
|
+
scope?: string;
|
|
1415
|
+
/** Severity level (P0 | P1 | P2 | P3). */
|
|
1416
|
+
severity?: string;
|
|
1417
|
+
/** Bypass BRAIN duplicate-task rejection (audited). */
|
|
1418
|
+
forceDuplicate?: boolean;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
/**
|
|
1422
|
+
* JSON Schema draft-07 document describing the accepted input shape for
|
|
1423
|
+
* the `tasks.add` mutate operation.
|
|
1424
|
+
*
|
|
1425
|
+
* Mirrors {@link TasksAddParams} field-for-field. Set
|
|
1426
|
+
* `additionalProperties: false` so a typo'd flag fails fast with a typed
|
|
1427
|
+
* `E_VAL_ADDITIONALPROPERTIES` error instead of being silently dropped.
|
|
1428
|
+
*
|
|
1429
|
+
* @task T9917
|
|
1430
|
+
*/
|
|
1431
|
+
export const TASKS_ADD_INPUT_SCHEMA: JsonSchema = {
|
|
1432
|
+
type: 'object',
|
|
1433
|
+
required: ['title'],
|
|
1434
|
+
additionalProperties: false,
|
|
1435
|
+
properties: {
|
|
1436
|
+
title: { type: 'string', minLength: 1, maxLength: 500 },
|
|
1437
|
+
description: { type: 'string' },
|
|
1438
|
+
parent: { type: 'string' },
|
|
1439
|
+
depends: { type: 'array', items: { type: 'string' } },
|
|
1440
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
1441
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
1442
|
+
type: { type: 'string', enum: ['saga', 'epic', 'task', 'subtask'] },
|
|
1443
|
+
acceptance: { type: 'array', items: { type: 'string' } },
|
|
1444
|
+
phase: { type: 'string' },
|
|
1445
|
+
size: { type: 'string', enum: ['small', 'medium', 'large'] },
|
|
1446
|
+
notes: { type: 'string' },
|
|
1447
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
1448
|
+
dryRun: { type: 'boolean' },
|
|
1449
|
+
parentSearch: { type: 'string' },
|
|
1450
|
+
kind: {
|
|
1451
|
+
type: 'string',
|
|
1452
|
+
enum: ['work', 'research', 'experiment', 'bug', 'spike', 'release'],
|
|
1453
|
+
},
|
|
1454
|
+
scope: { type: 'string', enum: ['project', 'feature', 'unit'] },
|
|
1455
|
+
severity: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
|
|
1456
|
+
forceDuplicate: { type: 'boolean' },
|
|
1457
|
+
},
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Schema-first input contract for `tasks.add`.
|
|
1462
|
+
*
|
|
1463
|
+
* @task T9917
|
|
1464
|
+
*/
|
|
1465
|
+
export const tasksAddInputContract: OperationInputContract<TasksAddParams> = {
|
|
1466
|
+
operation: 'tasks.add',
|
|
1467
|
+
schema: TASKS_ADD_INPUT_SCHEMA,
|
|
1468
|
+
examples: [
|
|
1469
|
+
{
|
|
1470
|
+
name: 'minimal',
|
|
1471
|
+
value: { title: 'Ship E7-MUTATE-DX-SCHEMA-FIRST' },
|
|
1472
|
+
description: 'Smallest valid input — title is the only required field.',
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
name: 'subtask-with-acceptance',
|
|
1476
|
+
value: {
|
|
1477
|
+
title: 'Wire validator into add command',
|
|
1478
|
+
parent: 'T9903',
|
|
1479
|
+
priority: 'high',
|
|
1480
|
+
acceptance: ['validator runs before dispatch', 'errors surface in envelope'],
|
|
1481
|
+
},
|
|
1482
|
+
},
|
|
1483
|
+
],
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* JSON Schema draft-07 document describing the accepted input shape for
|
|
1488
|
+
* the `tasks.add-batch` mutate operation.
|
|
1489
|
+
*
|
|
1490
|
+
* Wraps N `tasks.add` specs in a single atomic transaction. `dryRun` is
|
|
1491
|
+
* hoisted to the batch level (single rollback unit), so the per-entry
|
|
1492
|
+
* schema OMITS `dryRun` deliberately.
|
|
1493
|
+
*
|
|
1494
|
+
* @task T9917
|
|
1495
|
+
*/
|
|
1496
|
+
export const TASKS_ADD_BATCH_INPUT_SCHEMA: JsonSchema = {
|
|
1497
|
+
type: 'object',
|
|
1498
|
+
required: ['tasks'],
|
|
1499
|
+
additionalProperties: false,
|
|
1500
|
+
properties: {
|
|
1501
|
+
tasks: {
|
|
1502
|
+
type: 'array',
|
|
1503
|
+
minItems: 1,
|
|
1504
|
+
items: {
|
|
1505
|
+
type: 'object',
|
|
1506
|
+
required: ['title'],
|
|
1507
|
+
additionalProperties: false,
|
|
1508
|
+
properties: {
|
|
1509
|
+
title: { type: 'string', minLength: 1, maxLength: 500 },
|
|
1510
|
+
description: { type: 'string' },
|
|
1511
|
+
parent: { type: 'string' },
|
|
1512
|
+
depends: { type: 'array', items: { type: 'string' } },
|
|
1513
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
1514
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
1515
|
+
type: { type: 'string', enum: ['saga', 'epic', 'task', 'subtask'] },
|
|
1516
|
+
acceptance: { type: 'array', items: { type: 'string' } },
|
|
1517
|
+
phase: { type: 'string' },
|
|
1518
|
+
size: { type: 'string', enum: ['small', 'medium', 'large'] },
|
|
1519
|
+
notes: { type: 'string' },
|
|
1520
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
1521
|
+
kind: {
|
|
1522
|
+
type: 'string',
|
|
1523
|
+
enum: ['work', 'research', 'experiment', 'bug', 'spike', 'release'],
|
|
1524
|
+
},
|
|
1525
|
+
scope: { type: 'string', enum: ['project', 'feature', 'unit'] },
|
|
1526
|
+
severity: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
|
|
1527
|
+
forceDuplicate: { type: 'boolean' },
|
|
1528
|
+
},
|
|
1529
|
+
},
|
|
1530
|
+
},
|
|
1531
|
+
defaultParent: { type: 'string' },
|
|
1532
|
+
dryRun: { type: 'boolean' },
|
|
1533
|
+
},
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Schema-first input contract for `tasks.add-batch`.
|
|
1538
|
+
*
|
|
1539
|
+
* @task T9917
|
|
1540
|
+
*/
|
|
1541
|
+
export const tasksAddBatchInputContract: OperationInputContract<TasksAddBatchParams> = {
|
|
1542
|
+
operation: 'tasks.add-batch',
|
|
1543
|
+
schema: TASKS_ADD_BATCH_INPUT_SCHEMA,
|
|
1544
|
+
examples: [
|
|
1545
|
+
{
|
|
1546
|
+
name: 'two-tasks',
|
|
1547
|
+
value: {
|
|
1548
|
+
tasks: [{ title: 'first task' }, { title: 'second task' }],
|
|
1549
|
+
},
|
|
1550
|
+
description: 'Atomic insert of two tasks under no shared parent.',
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
name: 'with-default-parent',
|
|
1554
|
+
value: {
|
|
1555
|
+
defaultParent: 'T9903',
|
|
1556
|
+
tasks: [{ title: 'a' }, { title: 'b', parent: 'T9914' }],
|
|
1557
|
+
},
|
|
1558
|
+
description: 'defaultParent applies to entries without an explicit parent.',
|
|
1559
|
+
},
|
|
1560
|
+
],
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* JSON Schema draft-07 document describing the accepted input shape for
|
|
1565
|
+
* the `tasks.update` mutate operation.
|
|
1566
|
+
*
|
|
1567
|
+
* Mirrors {@link TasksUpdateQueryParams}. `taskId` is REQUIRED. All other
|
|
1568
|
+
* fields are optional — `tasks.update` is a partial update; only the
|
|
1569
|
+
* fields supplied are mutated.
|
|
1570
|
+
*
|
|
1571
|
+
* @task T9917
|
|
1572
|
+
*/
|
|
1573
|
+
export const TASKS_UPDATE_INPUT_SCHEMA: JsonSchema = {
|
|
1574
|
+
type: 'object',
|
|
1575
|
+
required: ['taskId'],
|
|
1576
|
+
additionalProperties: false,
|
|
1577
|
+
properties: {
|
|
1578
|
+
taskId: { type: 'string', minLength: 1 },
|
|
1579
|
+
title: { type: 'string', minLength: 1, maxLength: 500 },
|
|
1580
|
+
description: { type: 'string' },
|
|
1581
|
+
status: {
|
|
1582
|
+
type: 'string',
|
|
1583
|
+
enum: ['pending', 'active', 'blocked', 'done', 'cancelled'],
|
|
1584
|
+
},
|
|
1585
|
+
priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'] },
|
|
1586
|
+
notes: { type: 'string' },
|
|
1587
|
+
labels: { type: 'array', items: { type: 'string' } },
|
|
1588
|
+
addLabels: { type: 'array', items: { type: 'string' } },
|
|
1589
|
+
removeLabels: { type: 'array', items: { type: 'string' } },
|
|
1590
|
+
depends: { type: 'array', items: { type: 'string' } },
|
|
1591
|
+
addDepends: { type: 'array', items: { type: 'string' } },
|
|
1592
|
+
removeDepends: { type: 'array', items: { type: 'string' } },
|
|
1593
|
+
acceptance: { type: 'array', items: { type: 'string' } },
|
|
1594
|
+
parent: { type: ['string', 'null'] },
|
|
1595
|
+
type: { type: 'string', enum: ['saga', 'epic', 'task', 'subtask'] },
|
|
1596
|
+
size: { type: 'string', enum: ['small', 'medium', 'large'] },
|
|
1597
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
1598
|
+
addFiles: { type: 'array', items: { type: 'string' } },
|
|
1599
|
+
removeFiles: { type: 'array', items: { type: 'string' } },
|
|
1600
|
+
pipelineStage: { type: 'string' },
|
|
1601
|
+
kind: {
|
|
1602
|
+
type: 'string',
|
|
1603
|
+
enum: ['work', 'research', 'experiment', 'bug', 'spike', 'release'],
|
|
1604
|
+
},
|
|
1605
|
+
scope: { type: 'string', enum: ['project', 'feature', 'unit'] },
|
|
1606
|
+
severity: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
|
|
1607
|
+
reason: { type: 'string' },
|
|
1608
|
+
dependsWaiver: { type: 'string' },
|
|
1609
|
+
clearBlockedBy: { type: 'boolean' },
|
|
1610
|
+
relates: {
|
|
1611
|
+
type: 'array',
|
|
1612
|
+
items: {
|
|
1613
|
+
type: 'object',
|
|
1614
|
+
required: ['taskId', 'type'],
|
|
1615
|
+
additionalProperties: false,
|
|
1616
|
+
properties: {
|
|
1617
|
+
taskId: { type: 'string', minLength: 1 },
|
|
1618
|
+
type: { type: 'string', minLength: 1 },
|
|
1619
|
+
reason: { type: 'string' },
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1623
|
+
addRelates: {
|
|
1624
|
+
type: 'array',
|
|
1625
|
+
items: {
|
|
1626
|
+
type: 'object',
|
|
1627
|
+
required: ['taskId', 'type'],
|
|
1628
|
+
additionalProperties: false,
|
|
1629
|
+
properties: {
|
|
1630
|
+
taskId: { type: 'string', minLength: 1 },
|
|
1631
|
+
type: { type: 'string', minLength: 1 },
|
|
1632
|
+
reason: { type: 'string' },
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
removeRelates: { type: 'array', items: { type: 'string' } },
|
|
1637
|
+
},
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Schema-first input contract for `tasks.update`.
|
|
1642
|
+
*
|
|
1643
|
+
* @task T9917
|
|
1644
|
+
*/
|
|
1645
|
+
export const tasksUpdateInputContract: OperationInputContract<TasksUpdateQueryParams> = {
|
|
1646
|
+
operation: 'tasks.update',
|
|
1647
|
+
schema: TASKS_UPDATE_INPUT_SCHEMA,
|
|
1648
|
+
examples: [
|
|
1649
|
+
{
|
|
1650
|
+
name: 'set-status',
|
|
1651
|
+
value: { taskId: 'T9917', status: 'active' },
|
|
1652
|
+
description: 'Transition status to active.',
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: 'add-labels-incrementally',
|
|
1656
|
+
value: {
|
|
1657
|
+
taskId: 'T9917',
|
|
1658
|
+
addLabels: ['saga.t9855', 'epic.t9903'],
|
|
1659
|
+
},
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract tests for the TemplateManifest schema.
|
|
3
|
+
*
|
|
4
|
+
* Asserts:
|
|
5
|
+
*
|
|
6
|
+
* 1. A valid entry for every {@link TemplateKind} parses successfully.
|
|
7
|
+
* 2. An entry with an unknown `kind` is rejected.
|
|
8
|
+
* 3. An entry missing `installPath` is rejected.
|
|
9
|
+
* 4. The {@link PlaceholderSpec} `source` field is exhaustively typed —
|
|
10
|
+
* every member of {@link PLACEHOLDER_SOURCES} parses, and a bogus
|
|
11
|
+
* source value is rejected.
|
|
12
|
+
*
|
|
13
|
+
* @task T9875
|
|
14
|
+
* @epic T9874
|
|
15
|
+
* @saga T9855
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, it } from 'vitest';
|
|
19
|
+
import {
|
|
20
|
+
PLACEHOLDER_SOURCES,
|
|
21
|
+
PlaceholderSpecSchema,
|
|
22
|
+
TEMPLATE_KINDS,
|
|
23
|
+
type TemplateKind,
|
|
24
|
+
type TemplateManifestEntry,
|
|
25
|
+
TemplateManifestEntrySchema,
|
|
26
|
+
} from '../manifest.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a valid baseline entry parameterized by `kind` so each
|
|
30
|
+
* assertion below stays minimal.
|
|
31
|
+
*/
|
|
32
|
+
function baseEntry(kind: TemplateKind): TemplateManifestEntry {
|
|
33
|
+
return {
|
|
34
|
+
id: `sample-${kind}`,
|
|
35
|
+
kind,
|
|
36
|
+
sourcePath: `packages/core/templates/${kind}s/sample.tmpl`,
|
|
37
|
+
installPath: `.cleo/${kind}s/sample`,
|
|
38
|
+
substitution: 'regex-tmpl',
|
|
39
|
+
placeholders: [
|
|
40
|
+
{
|
|
41
|
+
name: 'NODE_VERSION',
|
|
42
|
+
source: 'project-context',
|
|
43
|
+
sourcePath: 'engines.node',
|
|
44
|
+
defaultValue: 24,
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
updateStrategy: 'overwrite-on-bump',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('TemplateManifestEntrySchema (T9875)', () => {
|
|
52
|
+
it('accepts a valid entry for every TemplateKind', () => {
|
|
53
|
+
for (const kind of TEMPLATE_KINDS) {
|
|
54
|
+
const entry = baseEntry(kind);
|
|
55
|
+
const parsed = TemplateManifestEntrySchema.parse(entry);
|
|
56
|
+
expect(parsed.kind, `kind '${kind}' must round-trip`).toBe(kind);
|
|
57
|
+
expect(parsed.installPath).toBe(entry.installPath);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects an entry with an unknown kind', () => {
|
|
62
|
+
const bogus = {
|
|
63
|
+
...baseEntry('workflow'),
|
|
64
|
+
kind: 'not-a-real-kind',
|
|
65
|
+
};
|
|
66
|
+
const result = TemplateManifestEntrySchema.safeParse(bogus);
|
|
67
|
+
expect(result.success).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('rejects an entry missing installPath', () => {
|
|
71
|
+
const { installPath: _drop, ...rest } = baseEntry('config');
|
|
72
|
+
const result = TemplateManifestEntrySchema.safeParse(rest);
|
|
73
|
+
expect(result.success).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('accepts an entry whose placeholders array is empty', () => {
|
|
77
|
+
const entry = { ...baseEntry('doc'), placeholders: [] };
|
|
78
|
+
const result = TemplateManifestEntrySchema.safeParse(entry);
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('accepts a defaultValue of every supported literal type and null', () => {
|
|
83
|
+
const types = [
|
|
84
|
+
{ defaultValue: 'string-default' },
|
|
85
|
+
{ defaultValue: 42 },
|
|
86
|
+
{ defaultValue: true },
|
|
87
|
+
{ defaultValue: null },
|
|
88
|
+
{},
|
|
89
|
+
] as const;
|
|
90
|
+
for (const extra of types) {
|
|
91
|
+
const result = PlaceholderSpecSchema.safeParse({
|
|
92
|
+
name: 'X',
|
|
93
|
+
source: 'literal',
|
|
94
|
+
sourcePath: 'X',
|
|
95
|
+
...extra,
|
|
96
|
+
});
|
|
97
|
+
expect(result.success, `defaultValue case ${JSON.stringify(extra)} must parse`).toBe(true);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('PlaceholderSpecSchema source enum (T9875)', () => {
|
|
103
|
+
it('exhaustively accepts every member of PLACEHOLDER_SOURCES', () => {
|
|
104
|
+
for (const source of PLACEHOLDER_SOURCES) {
|
|
105
|
+
const result = PlaceholderSpecSchema.safeParse({
|
|
106
|
+
name: 'PLACEHOLDER',
|
|
107
|
+
source,
|
|
108
|
+
sourcePath: 'some.path',
|
|
109
|
+
});
|
|
110
|
+
expect(result.success, `source '${source}' must parse`).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('rejects an unknown source value', () => {
|
|
115
|
+
const result = PlaceholderSpecSchema.safeParse({
|
|
116
|
+
name: 'PLACEHOLDER',
|
|
117
|
+
source: 'not-a-source',
|
|
118
|
+
sourcePath: 'some.path',
|
|
119
|
+
});
|
|
120
|
+
expect(result.success).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects an empty name', () => {
|
|
124
|
+
const result = PlaceholderSpecSchema.safeParse({
|
|
125
|
+
name: '',
|
|
126
|
+
source: 'project-context',
|
|
127
|
+
sourcePath: 'some.path',
|
|
128
|
+
});
|
|
129
|
+
expect(result.success).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('rejects an empty sourcePath', () => {
|
|
133
|
+
const result = PlaceholderSpecSchema.safeParse({
|
|
134
|
+
name: 'X',
|
|
135
|
+
source: 'project-context',
|
|
136
|
+
sourcePath: '',
|
|
137
|
+
});
|
|
138
|
+
expect(result.success).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TemplateManifest contract — SSoT for every template CLEO ships, installs,
|
|
3
|
+
* or rewrites during `cleo init` / `cleo upgrade` / `cleo scaffold-*`.
|
|
4
|
+
*
|
|
5
|
+
* A {@link TemplateManifestEntry} declares one template file: where its
|
|
6
|
+
* source lives in the monorepo, where it installs into a consumer project,
|
|
7
|
+
* how its placeholders are substituted, which placeholders it accepts, and
|
|
8
|
+
* how it MUST be reconciled when the shipped version moves ahead of the
|
|
9
|
+
* deployed copy.
|
|
10
|
+
*
|
|
11
|
+
* This contract is the leaf type. The CORE-side template registry
|
|
12
|
+
* (T9877) consumes it; the CLI verbs (`cleo init --workflows`,
|
|
13
|
+
* `cleo upgrade`, the scaffold sweeper) read the manifest at runtime to
|
|
14
|
+
* decide what to write, what to skip, and what to diff-prompt the user on.
|
|
15
|
+
*
|
|
16
|
+
* @example minimal workflow template entry
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { TemplateManifestEntrySchema, type TemplateManifestEntry } from '@cleocode/contracts';
|
|
19
|
+
*
|
|
20
|
+
* const entry: TemplateManifestEntry = TemplateManifestEntrySchema.parse({
|
|
21
|
+
* id: 'ci.yml',
|
|
22
|
+
* kind: 'workflow',
|
|
23
|
+
* sourcePath: 'packages/core/templates/workflows/ci.yml.tmpl',
|
|
24
|
+
* installPath: '.github/workflows/ci.yml',
|
|
25
|
+
* substitution: 'regex-tmpl',
|
|
26
|
+
* placeholders: [
|
|
27
|
+
* {
|
|
28
|
+
* name: 'NODE_VERSION',
|
|
29
|
+
* source: 'project-context',
|
|
30
|
+
* sourcePath: 'engines.node',
|
|
31
|
+
* defaultValue: 24,
|
|
32
|
+
* },
|
|
33
|
+
* ],
|
|
34
|
+
* updateStrategy: 'overwrite-on-bump',
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @task T9875
|
|
39
|
+
* @epic T9874
|
|
40
|
+
* @saga T9855
|
|
41
|
+
* @see ADR-076 (canonical doc routing — same boundary discipline applies here)
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { z } from 'zod';
|
|
45
|
+
|
|
46
|
+
// ─── TemplateKind ─────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Allowed categories of template the manifest can describe.
|
|
50
|
+
*
|
|
51
|
+
* - `workflow` — GitHub Actions / CI workflow files.
|
|
52
|
+
* - `config` — Configuration files (e.g. `.editorconfig`, `tsconfig.json`).
|
|
53
|
+
* - `agent` — Agent definition files shipped to `.claude/agents/` etc.
|
|
54
|
+
* - `skill` — Skill markdown shipped to `.claude/skills/` etc.
|
|
55
|
+
* - `provider` — Provider-specific bootstrap files (Anthropic, OpenAI, …).
|
|
56
|
+
* - `doc` — Documentation scaffolds (e.g. README.md, AGENTS.md).
|
|
57
|
+
*/
|
|
58
|
+
export const TEMPLATE_KINDS = ['workflow', 'config', 'agent', 'skill', 'provider', 'doc'] as const;
|
|
59
|
+
|
|
60
|
+
/** Discriminator for the category of file a template represents. */
|
|
61
|
+
export type TemplateKind = (typeof TEMPLATE_KINDS)[number];
|
|
62
|
+
|
|
63
|
+
// ─── TemplateSubstitution ─────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Allowed substitution strategies a template can request.
|
|
67
|
+
*
|
|
68
|
+
* - `regex-tmpl` — `{{KEY}}` placeholder substitution via regex.
|
|
69
|
+
* - `static` — No substitution; the file is copied byte-for-byte.
|
|
70
|
+
* - `json-merge` — The template is parsed as JSON and merged into any
|
|
71
|
+
* existing JSON file at `installPath`; placeholders inside
|
|
72
|
+
* string values still go through `regex-tmpl`.
|
|
73
|
+
*/
|
|
74
|
+
export const TEMPLATE_SUBSTITUTIONS = ['regex-tmpl', 'static', 'json-merge'] as const;
|
|
75
|
+
|
|
76
|
+
/** Strategy the installer uses to materialize a template's `sourcePath`. */
|
|
77
|
+
export type TemplateSubstitution = (typeof TEMPLATE_SUBSTITUTIONS)[number];
|
|
78
|
+
|
|
79
|
+
// ─── UpdateStrategy ───────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* How an installed template is reconciled with the shipped copy on
|
|
83
|
+
* `cleo upgrade` / `cleo init --force`.
|
|
84
|
+
*
|
|
85
|
+
* - `overwrite-on-bump` — Always overwrite when the manifest version is
|
|
86
|
+
* newer than the deployed copy.
|
|
87
|
+
* - `diff-prompt` — Show a diff and ask the user before overwriting.
|
|
88
|
+
* - `immutable` — Never overwrite once installed.
|
|
89
|
+
* - `manifest-merge` — Apply structural merge (e.g. JSON deep-merge,
|
|
90
|
+
* respecting existing user keys).
|
|
91
|
+
*/
|
|
92
|
+
export const TEMPLATE_UPDATE_STRATEGIES = [
|
|
93
|
+
'overwrite-on-bump',
|
|
94
|
+
'diff-prompt',
|
|
95
|
+
'immutable',
|
|
96
|
+
'manifest-merge',
|
|
97
|
+
] as const;
|
|
98
|
+
|
|
99
|
+
/** Reconciliation policy for an installed template on upgrade. */
|
|
100
|
+
export type UpdateStrategy = (typeof TEMPLATE_UPDATE_STRATEGIES)[number];
|
|
101
|
+
|
|
102
|
+
// ─── PlaceholderSpec ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Allowed resolver sources a {@link PlaceholderSpec} can reference.
|
|
106
|
+
*
|
|
107
|
+
* - `project-context` — `.cleo/project-context.json` (detected ecosystem hints).
|
|
108
|
+
* - `project-info` — `.cleo/project-info.json` (per-project metadata).
|
|
109
|
+
* - `.cleo/config` — Per-project config (`<projectRoot>/.cleo/config.json`).
|
|
110
|
+
* - `~/.cleo/config` — User-global config (`$XDG_CONFIG_HOME/cleo/config.json`).
|
|
111
|
+
* - `tool-resolver` — Resolved via the per-language tool resolver
|
|
112
|
+
* (e.g. `pnpm` vs `npm` chosen by `primaryType`).
|
|
113
|
+
* - `literal` — Hard-coded value baked into the manifest entry.
|
|
114
|
+
*/
|
|
115
|
+
export const PLACEHOLDER_SOURCES = [
|
|
116
|
+
'project-context',
|
|
117
|
+
'project-info',
|
|
118
|
+
'.cleo/config',
|
|
119
|
+
'~/.cleo/config',
|
|
120
|
+
'tool-resolver',
|
|
121
|
+
'literal',
|
|
122
|
+
] as const;
|
|
123
|
+
|
|
124
|
+
/** Resolver source enum for {@link PlaceholderSpec.source}. */
|
|
125
|
+
export type PlaceholderSource = (typeof PLACEHOLDER_SOURCES)[number];
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Zod schema for a single placeholder declaration on a template entry.
|
|
129
|
+
*
|
|
130
|
+
* Each placeholder is resolved at install time by reading `sourcePath` from
|
|
131
|
+
* the named `source`. If resolution returns `undefined`, the installer
|
|
132
|
+
* falls back to `defaultValue`; if neither yields a value, installation
|
|
133
|
+
* fails with `E_PLACEHOLDER_UNRESOLVED`.
|
|
134
|
+
*/
|
|
135
|
+
export const PlaceholderSpecSchema = z.object({
|
|
136
|
+
/**
|
|
137
|
+
* Placeholder identifier as it appears in the template body
|
|
138
|
+
* (e.g. `NODE_VERSION` matches `{{NODE_VERSION}}`).
|
|
139
|
+
*/
|
|
140
|
+
name: z.string().min(1, 'placeholder name must be non-empty'),
|
|
141
|
+
/** Resolver source the installer consults for this placeholder. */
|
|
142
|
+
source: z.enum(PLACEHOLDER_SOURCES),
|
|
143
|
+
/**
|
|
144
|
+
* Path expression evaluated against `source` (e.g. `engines.node` against
|
|
145
|
+
* `project-context`, `defaults.branchModel` against `.cleo/config`).
|
|
146
|
+
* For `literal` source, this MAY be the literal value's identifier.
|
|
147
|
+
*/
|
|
148
|
+
sourcePath: z.string().min(1, 'placeholder sourcePath must be non-empty'),
|
|
149
|
+
/**
|
|
150
|
+
* Fallback value used when `source[sourcePath]` resolves to `undefined`.
|
|
151
|
+
* `null` is permitted to explicitly mark "no default — failure required".
|
|
152
|
+
*/
|
|
153
|
+
defaultValue: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Zod-inferred input shape for a placeholder declaration.
|
|
158
|
+
*
|
|
159
|
+
* Identical to {@link PlaceholderSpec} — there are no schema transforms.
|
|
160
|
+
*/
|
|
161
|
+
export type PlaceholderSpecInput = z.infer<typeof PlaceholderSpecSchema>;
|
|
162
|
+
|
|
163
|
+
/** Declared placeholder on a {@link TemplateManifestEntry}. */
|
|
164
|
+
export type PlaceholderSpec = PlaceholderSpecInput;
|
|
165
|
+
|
|
166
|
+
// ─── TemplateManifestEntry ────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Zod schema for a single template manifest entry.
|
|
170
|
+
*
|
|
171
|
+
* Validates the full descriptor a template registry consumer needs to
|
|
172
|
+
* install a template file:
|
|
173
|
+
*
|
|
174
|
+
* - `id` — Stable identifier (kebab-case, matches install basename
|
|
175
|
+
* or a domain-unique slug).
|
|
176
|
+
* - `kind` — One of {@link TEMPLATE_KINDS}.
|
|
177
|
+
* - `sourcePath` — Repo-relative path of the template source.
|
|
178
|
+
* - `installPath` — Project-relative path where the rendered file lands.
|
|
179
|
+
* - `substitution` — One of {@link TEMPLATE_SUBSTITUTIONS}.
|
|
180
|
+
* - `placeholders` — Declared placeholders the installer must resolve.
|
|
181
|
+
* - `updateStrategy` — Reconciliation policy on upgrade.
|
|
182
|
+
*/
|
|
183
|
+
export const TemplateManifestEntrySchema = z.object({
|
|
184
|
+
/** Stable identifier for this template entry. */
|
|
185
|
+
id: z.string().min(1, 'id must be non-empty'),
|
|
186
|
+
/** Category of file this template represents. */
|
|
187
|
+
kind: z.enum(TEMPLATE_KINDS),
|
|
188
|
+
/** Repo-relative path of the template source file. */
|
|
189
|
+
sourcePath: z.string().min(1, 'sourcePath must be non-empty'),
|
|
190
|
+
/** Project-relative path where the rendered template installs. */
|
|
191
|
+
installPath: z.string().min(1, 'installPath must be non-empty'),
|
|
192
|
+
/** Substitution strategy the installer applies to `sourcePath`. */
|
|
193
|
+
substitution: z.enum(TEMPLATE_SUBSTITUTIONS),
|
|
194
|
+
/** Declared placeholders this template requires. May be empty. */
|
|
195
|
+
placeholders: z.array(PlaceholderSpecSchema),
|
|
196
|
+
/** Reconciliation policy on upgrade. */
|
|
197
|
+
updateStrategy: z.enum(TEMPLATE_UPDATE_STRATEGIES),
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Zod-inferred input shape for a template manifest entry.
|
|
202
|
+
*
|
|
203
|
+
* Identical to {@link TemplateManifestEntry} — there are no schema transforms.
|
|
204
|
+
*/
|
|
205
|
+
export type TemplateManifestEntryInput = z.infer<typeof TemplateManifestEntrySchema>;
|
|
206
|
+
|
|
207
|
+
/** Public type for a fully-validated template manifest entry. */
|
|
208
|
+
export type TemplateManifestEntry = TemplateManifestEntryInput;
|