@cleocode/contracts 2026.6.6 → 2026.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/__tests__/operation-def.test.js.map +1 -1
  2. package/dist/db-recovery.d.ts +63 -0
  3. package/dist/db-recovery.d.ts.map +1 -1
  4. package/dist/dispatch/operation-def.d.ts +42 -0
  5. package/dist/dispatch/operation-def.d.ts.map +1 -1
  6. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  7. package/dist/dispatch/operations-registry.js +7 -1
  8. package/dist/dispatch/operations-registry.js.map +1 -1
  9. package/dist/docs/provenance.d.ts +6 -2
  10. package/dist/docs/provenance.d.ts.map +1 -1
  11. package/dist/docs/provenance.js +1 -0
  12. package/dist/docs/provenance.js.map +1 -1
  13. package/dist/docs/read.d.ts +148 -0
  14. package/dist/docs/read.d.ts.map +1 -0
  15. package/dist/docs/read.js +57 -0
  16. package/dist/docs/read.js.map +1 -0
  17. package/dist/index.d.ts +5 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +2 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/llm/provider-profile.d.ts +54 -0
  22. package/dist/llm/provider-profile.d.ts.map +1 -1
  23. package/dist/operations/__tests__/output-contracts.test.d.ts +16 -0
  24. package/dist/operations/__tests__/output-contracts.test.d.ts.map +1 -0
  25. package/dist/operations/__tests__/output-contracts.test.js +103 -0
  26. package/dist/operations/__tests__/output-contracts.test.js.map +1 -0
  27. package/dist/operations/docs.d.ts +13 -3
  28. package/dist/operations/docs.d.ts.map +1 -1
  29. package/dist/operations/docs.js.map +1 -1
  30. package/dist/operations/output-contract.d.ts +127 -0
  31. package/dist/operations/output-contract.d.ts.map +1 -0
  32. package/dist/operations/output-contract.js +41 -0
  33. package/dist/operations/output-contract.js.map +1 -0
  34. package/dist/operations/output-contracts-data.d.ts +35 -0
  35. package/dist/operations/output-contracts-data.d.ts.map +1 -0
  36. package/dist/operations/output-contracts-data.js +328 -0
  37. package/dist/operations/output-contracts-data.js.map +1 -0
  38. package/dist/operations/tasks.d.ts +14 -4
  39. package/dist/operations/tasks.d.ts.map +1 -1
  40. package/dist/operations/tasks.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  43. package/src/__tests__/operation-def.test.ts +5 -0
  44. package/src/db-recovery.ts +65 -0
  45. package/src/dispatch/operation-def.ts +44 -0
  46. package/src/dispatch/operations-registry.ts +7 -1
  47. package/src/docs/provenance.ts +4 -1
  48. package/src/docs/read.ts +131 -0
  49. package/src/index.ts +19 -0
  50. package/src/llm/provider-profile.ts +59 -0
  51. package/src/operations/__tests__/output-contracts.test.ts +125 -0
  52. package/src/operations/docs.ts +13 -3
  53. package/src/operations/output-contract.ts +140 -0
  54. package/src/operations/output-contracts-data.ts +356 -0
  55. package/src/operations/tasks.ts +14 -4
@@ -21,6 +21,8 @@ import { describe, expect, it } from 'vitest';
21
21
  import type { CanonicalDomain, Gateway, Tier } from '../dispatch/identity.js';
22
22
  import { CANONICAL_DOMAINS } from '../dispatch/identity.js';
23
23
  import type { OperationDef, Resolution } from '../dispatch/operation-def.js';
24
+ import type { OperationInputContract } from '../operations/input-contract.js';
25
+ import type { OperationOutputContract } from '../operations/output-contract.js';
24
26
  import type { ParamDef } from '../operations/params.js';
25
27
 
26
28
  // ─── Compile-time structural-equality helpers ───────────────────────
@@ -66,6 +68,9 @@ type _OperationDefShape = {
66
68
  requiredParams: string[];
67
69
  params?: ParamDef[];
68
70
  mcpExposed?: boolean;
71
+ // T11692 (DHQ-033 / DHQ-057) — optional schema-first input/output contracts.
72
+ inputSchema?: OperationInputContract<unknown>;
73
+ outputSchema?: OperationOutputContract;
69
74
  };
70
75
 
71
76
  type _AssertOperationDefPinned = AssertEquals1<Equals<OperationDef, _OperationDefShape>>;
@@ -142,3 +142,68 @@ export interface BackupRecoverResult {
142
142
  */
143
143
  dryRun: boolean;
144
144
  }
145
+
146
+ /**
147
+ * Per-role outcome of a `cleo doctor repair` pass (T11829 · DHQ-060).
148
+ *
149
+ * `doctor repair` first PROBES each role's live DB via `PRAGMA quick_check`
150
+ * (the same probe the recovery pipeline runs on snapshot candidates) and only
151
+ * invokes the {@link recoverMalformedDb} → quarantine → restore-from-snapshot →
152
+ * verify pipeline for roles that are actually malformed. This record captures
153
+ * what was observed and done for one role.
154
+ *
155
+ * @task T11829
156
+ * @epic T11833
157
+ * @saga T11242
158
+ * @public
159
+ */
160
+ export interface DoctorRepairRoleResult {
161
+ /** Canonical role inspected. */
162
+ role: DbRole;
163
+ /** Absolute path to the role's live DB file (resolved from `DB_INVENTORY`). */
164
+ dbPath: string;
165
+ /** `true` when the live DB file exists on disk. */
166
+ present: boolean;
167
+ /**
168
+ * `true` when the live DB passed `PRAGMA quick_check` (or was absent) — i.e.
169
+ * NO repair was needed. `false` when corruption was detected.
170
+ */
171
+ healthy: boolean;
172
+ /**
173
+ * `'skipped'` — healthy or absent, nothing done;
174
+ * `'would-repair'` — `--dry-run`, corruption detected, repair NOT performed;
175
+ * `'repaired'` — corruption detected and snapshot restore succeeded;
176
+ * `'failed'` — corruption detected but recovery could not complete.
177
+ */
178
+ action: 'skipped' | 'would-repair' | 'repaired' | 'failed';
179
+ /** Absolute path to the snapshot restored (or that would be), when applicable. */
180
+ restoredFrom: string | null;
181
+ /** Absolute path to the quarantined corrupt DB, when a repair was performed. */
182
+ quarantinedTo: string | null;
183
+ /** Approximate data-loss window in hours, when a snapshot timestamp was parsed. */
184
+ dataLossWindowHours: number | null;
185
+ /** Human-readable detail (probe outcome, error reason, or restore summary). */
186
+ detail: string;
187
+ }
188
+
189
+ /**
190
+ * Aggregate result of a `cleo doctor repair` invocation across one or more roles
191
+ * (T11829 · DHQ-060).
192
+ *
193
+ * @task T11829
194
+ * @epic T11833
195
+ * @saga T11242
196
+ * @public
197
+ */
198
+ export interface DoctorRepairResult {
199
+ /** `true` when the run was a `--dry-run` (no files mutated). */
200
+ dryRun: boolean;
201
+ /** Per-role outcomes. */
202
+ roles: DoctorRepairRoleResult[];
203
+ /** Count of roles found malformed (`!healthy`). */
204
+ malformedCount: number;
205
+ /** Count of roles actually repaired this run. */
206
+ repairedCount: number;
207
+ /** Count of roles where a needed repair FAILED. */
208
+ failedCount: number;
209
+ }
@@ -28,6 +28,8 @@
28
28
  * @since SG-ARCH-SOLID Saga T9831 · E-CONTRACTS-FOUNDATION T9832 · T9954 (Phase 0b)
29
29
  */
30
30
 
31
+ import type { OperationInputContract } from '../operations/input-contract.js';
32
+ import type { OperationOutputContract } from '../operations/output-contract.js';
31
33
  import type { ParamDef } from '../operations/params.js';
32
34
  import type { CanonicalDomain, Gateway, Tier } from './identity.js';
33
35
 
@@ -92,6 +94,48 @@ export interface OperationDef {
92
94
  * @task T11448
93
95
  */
94
96
  mcpExposed?: boolean;
97
+
98
+ /**
99
+ * Schema-first INPUT contract for this operation (DHQ-033 / T11692).
100
+ *
101
+ * Declares the accepted request payload shape as a JSON Schema draft-07
102
+ * document with worked examples. When present, the dispatch/CLI layer
103
+ * validates the incoming payload against `inputSchema.schema` and rejects
104
+ * unknown keys LOUDLY (the schemas set `additionalProperties: false`), so a
105
+ * typo'd or unsupported field — e.g. passing `relates` to `tasks.add-batch`,
106
+ * which only accepts `depends` — fails fast with a typed {@link ValidationError}
107
+ * instead of being silently dropped.
108
+ *
109
+ * **Optional / incremental**: absent means "no declared input contract yet".
110
+ * The canonical SSoT registry of contracts is keyed by `<domain>.<operation>`
111
+ * (INPUT_CONTRACTS in core); this inline field lets a definition carry its
112
+ * own contract and is resolved by the SDK `describeOperation` when set.
113
+ *
114
+ * @see OperationInputContract
115
+ * @task T11692 — DHQ-033 input shape SSoT
116
+ */
117
+ inputSchema?: OperationInputContract<unknown>;
118
+
119
+ /**
120
+ * Schema-first OUTPUT contract for this operation (DHQ-057 / T11692).
121
+ *
122
+ * Declares the shape of the LAFS envelope's `data` payload as a JSON Schema
123
+ * draft-07 document, PLUS the curated list of valid `--field` JSON pointers.
124
+ * This is the SSoT that lets an agent predict the result shape instead of
125
+ * hand-sniffing it per verb — e.g. it encodes that `tasks.show` returns the
126
+ * task under `task`, so the correct pointer is `/data/task/title` and NOT
127
+ * `/data/title` (the latter is the exact `E_FIELD_NOT_FOUND` failure this
128
+ * field exists to prevent).
129
+ *
130
+ * **Optional / incremental**: absent means "no declared output contract yet".
131
+ * The canonical SSoT registry is keyed by `<domain>.<operation>`
132
+ * (OUTPUT_CONTRACTS in contracts); this inline field lets a definition carry
133
+ * its own contract and is resolved by the SDK `describeOperation` when set.
134
+ *
135
+ * @see OperationOutputContract
136
+ * @task T11692 — DHQ-057 output shape SSoT
137
+ */
138
+ outputSchema?: OperationOutputContract;
95
139
  }
96
140
 
97
141
  // ── Resolution ───────────────────────────────────────────────────────
@@ -7117,7 +7117,7 @@ export const OPERATIONS: OperationDef[] = [
7117
7117
  domain: 'docs',
7118
7118
  operation: 'add',
7119
7119
  description:
7120
- 'docs.add (mutate) — attach a local file or URL to a CLEO owner entity (task, session, observation)',
7120
+ 'docs.add (mutate) — attach a local file, URL, or inline content to a CLEO owner entity (task, session, observation)',
7121
7121
  tier: 1,
7122
7122
  idempotent: false,
7123
7123
  sessionRequired: false,
@@ -7141,6 +7141,12 @@ export const OPERATIONS: OperationDef[] = [
7141
7141
  required: false,
7142
7142
  description: 'Remote URL to attach',
7143
7143
  },
7144
+ {
7145
+ name: 'content',
7146
+ type: 'string' as const,
7147
+ required: false,
7148
+ description: 'Inline document body (T10965); mutually exclusive with file/url',
7149
+ },
7144
7150
  {
7145
7151
  name: 'desc',
7146
7152
  type: 'string' as const,
@@ -74,6 +74,7 @@ export const PROVENANCE_NODE_KINDS = [
74
74
  * - `related-task` — child task is related (non-parent) to parent
75
75
  * - `linked-decision` — node references a BRAIN decision
76
76
  * - `derived-from` — node was derived from a prior artifact
77
+ * - `shares-topic` — two docs co-occur on a canonical topic slug (T11826)
77
78
  *
78
79
  * @task T10166
79
80
  */
@@ -83,7 +84,8 @@ export type ProvenanceEdgeRelation =
83
84
  | 'superseded-by'
84
85
  | 'related-task'
85
86
  | 'linked-decision'
86
- | 'derived-from';
87
+ | 'derived-from'
88
+ | 'shares-topic';
87
89
 
88
90
  /**
89
91
  * Frozen const array backing {@link ProvenanceEdgeRelation} — used by the
@@ -98,6 +100,7 @@ export const PROVENANCE_EDGE_RELATIONS = [
98
100
  'related-task',
99
101
  'linked-decision',
100
102
  'derived-from',
103
+ 'shares-topic',
101
104
  ] as const satisfies ReadonlyArray<ProvenanceEdgeRelation>;
102
105
 
103
106
  /**
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `docs.read` core-SDK contract — the envelope-typed surface an external
3
+ * consumer (the Obsidian plugin, `cleo docs view`) calls to render a single
4
+ * CLEO doc directly from `cleo.db`, the SOLE doc authority (saga T11778).
5
+ *
6
+ * The response carries the doc **body** plus its full provenance **frontmatter**
7
+ * (slug, version anchors, supersession pointers, related tasks, topics). Binary
8
+ * or non-UTF-8 bodies are surfaced as base64 (T11825 AC2) so a consumer can
9
+ * render images/PDFs/files stored as blobs without a second round-trip.
10
+ *
11
+ * Part of the T10400 core-SDK API standard — this is a typed contract, not an
12
+ * ad-hoc shape (T11825 AC3).
13
+ *
14
+ * @task T11825 (Epic T11781 / Saga T11778)
15
+ * @adr ADR-078 — Docs SSoT as provenance graph
16
+ */
17
+
18
+ import { z } from 'zod';
19
+
20
+ /**
21
+ * Provenance frontmatter for a doc, sourced from the `attachments` columns that
22
+ * back the docs SSoT. Mirrors what a published doc's YAML frontmatter would
23
+ * carry, but read live from the DB rather than a file.
24
+ *
25
+ * @task T11825
26
+ */
27
+ export interface DocFrontmatter {
28
+ /** Stable human-readable slug (`attachments.slug`). */
29
+ readonly slug: string;
30
+ /** Doc-kind taxonomy tag (e.g. `adr`, `spec`), or null for untyped blobs. */
31
+ readonly kind: string | null;
32
+ /** Display title (falls back to slug). */
33
+ readonly title: string | null;
34
+ /** One-sentence human summary, distinct from the body. */
35
+ readonly summary: string | null;
36
+ /** Document workflow state (`draft | active | superseded | …`). */
37
+ readonly lifecycleStatus: string;
38
+ /** Sequential doc-version counter for this slug (`attachments.doc_version`). */
39
+ readonly docVersion: number;
40
+ /** CLEO release version that wrote this row (`attachments.owner_version`). */
41
+ readonly ownerVersion: string | null;
42
+ /** Slug of the doc this one supersedes, or null. */
43
+ readonly supersedes: string | null;
44
+ /** Slug of the doc that supersedes this one, or null. */
45
+ readonly supersededBy: string | null;
46
+ /** Canonical topic slugs this doc carries (`attachments.topics`). */
47
+ readonly topics: readonly string[];
48
+ /** `T####` task ids this doc relates to (`attachments.related_tasks`). */
49
+ readonly relatedTasks: readonly string[];
50
+ /** SHA-256 content address of the body. */
51
+ readonly sha256: string;
52
+ /** ISO-8601 creation instant. */
53
+ readonly createdAt: string;
54
+ }
55
+
56
+ /**
57
+ * The body payload of a doc read.
58
+ *
59
+ * Exactly one of {@link text} / {@link base64} is populated: UTF-8-decodable
60
+ * content is surfaced as `text` with `encoding: 'utf-8'`; binary content is
61
+ * surfaced as `base64` with `encoding: 'base64'`.
62
+ *
63
+ * @task T11825
64
+ */
65
+ export interface DocBody {
66
+ /** How {@link DocReadResponse.body} content is encoded. */
67
+ readonly encoding: 'utf-8' | 'base64';
68
+ /** UTF-8 body text, present when `encoding === 'utf-8'`. */
69
+ readonly text?: string;
70
+ /** Base64-encoded body bytes, present when `encoding === 'base64'`. */
71
+ readonly base64?: string;
72
+ /** Decoded byte length of the body. */
73
+ readonly sizeBytes: number;
74
+ /** IANA MIME type, when known. */
75
+ readonly mimeType: string | null;
76
+ }
77
+
78
+ /**
79
+ * The full `docs.read` response envelope payload.
80
+ *
81
+ * @task T11825
82
+ */
83
+ export interface DocReadResponse {
84
+ /** Provenance frontmatter. */
85
+ readonly frontmatter: DocFrontmatter;
86
+ /** Rendered body (text or base64). */
87
+ readonly body: DocBody;
88
+ }
89
+
90
+ /** Zod schema for {@link DocFrontmatter}. */
91
+ export const docFrontmatterSchema = z.object({
92
+ slug: z.string(),
93
+ kind: z.string().nullable(),
94
+ title: z.string().nullable(),
95
+ summary: z.string().nullable(),
96
+ lifecycleStatus: z.string(),
97
+ docVersion: z.number().int(),
98
+ ownerVersion: z.string().nullable(),
99
+ supersedes: z.string().nullable(),
100
+ supersededBy: z.string().nullable(),
101
+ topics: z.array(z.string()).readonly(),
102
+ relatedTasks: z.array(z.string()).readonly(),
103
+ sha256: z.string(),
104
+ createdAt: z.string(),
105
+ });
106
+
107
+ /** Zod schema for {@link DocBody}. */
108
+ export const docBodySchema = z.object({
109
+ encoding: z.enum(['utf-8', 'base64']),
110
+ text: z.string().optional(),
111
+ base64: z.string().optional(),
112
+ sizeBytes: z.number().int().nonnegative(),
113
+ mimeType: z.string().nullable(),
114
+ });
115
+
116
+ /** Zod schema for {@link DocReadResponse}. */
117
+ export const docReadResponseSchema = z.object({
118
+ frontmatter: docFrontmatterSchema,
119
+ body: docBodySchema,
120
+ });
121
+
122
+ /**
123
+ * Type guard for {@link DocReadResponse}.
124
+ *
125
+ * @param value - Unknown value to validate.
126
+ * @returns True when `value` conforms to the `docs.read` response schema.
127
+ * @task T11825
128
+ */
129
+ export function isDocReadResponse(value: unknown): value is DocReadResponse {
130
+ return docReadResponseSchema.safeParse(value).success;
131
+ }
package/src/index.ts CHANGED
@@ -346,6 +346,8 @@ export type {
346
346
  BackupRecoverResult,
347
347
  DbRecoveredRowCounts,
348
348
  DbRecoveryResult,
349
+ DoctorRepairResult,
350
+ DoctorRepairRoleResult,
349
351
  } from './db-recovery.js';
350
352
  // === Dependency Registry Contracts ===
351
353
  export type {
@@ -398,6 +400,14 @@ export {
398
400
  provenanceSessionNodeSchema,
399
401
  provenanceTaskNodeSchema,
400
402
  } from './docs/provenance.js';
403
+ // === docs.read core-SDK Contract (T11825) ===
404
+ export type { DocBody, DocFrontmatter, DocReadResponse } from './docs/read.js';
405
+ export {
406
+ docBodySchema,
407
+ docFrontmatterSchema,
408
+ docReadResponseSchema,
409
+ isDocReadResponse,
410
+ } from './docs/read.js';
401
411
  // === DocsAccessor Contracts (T9063) ===
402
412
  export type {
403
413
  DocExportFormat,
@@ -1318,6 +1328,15 @@ export type {
1318
1328
  OrchestrateReportGroup,
1319
1329
  OrchestrateReportParams,
1320
1330
  } from './operations/orchestrate.js';
1331
+ // === Operation Output Contracts (T11692 / DHQ-057 — per-op result shape SSoT) ===
1332
+ // OUTPUT-side mirror of the input contracts. Surfaces the LAFS envelope `data`
1333
+ // shape + valid --field JSON pointers so agents stop guessing (e.g. the real
1334
+ // pointer is /data/task/title, not /data/title).
1335
+ export type {
1336
+ OperationOutputContract,
1337
+ OperationOutputContractRegistry,
1338
+ } from './operations/output-contract.js';
1339
+ export { OUTPUT_CONTRACTS } from './operations/output-contracts-data.js';
1321
1340
  // ParamDef contract — re-exported at top level (SSoT for all operation param descriptors)
1322
1341
  export type {
1323
1342
  CittyArgDef,
@@ -14,6 +14,17 @@ import type { ModelTransport, StoredAuthTypeWire } from '../operations/llm.js';
14
14
  import type { TransportMessage, TransportTool } from './normalized-response.js';
15
15
  import type { ProviderOAuthConfig } from './oauth.js';
16
16
 
17
+ /**
18
+ * Coarse provider capability tier used by the system-of-use router (E9).
19
+ *
20
+ * Ordered most→least capable: `frontier` (flagship reasoning), `standard`
21
+ * (general-purpose), `fast` (cheap/low-latency aux), `local` (on-device, e.g.
22
+ * Ollama). Sourced from the models.dev catalog (E8) when generated.
23
+ *
24
+ * @task T11756
25
+ */
26
+ export type ProviderTier = 'frontier' | 'standard' | 'fast' | 'local';
27
+
17
28
  /**
18
29
  * Describes a single LLM provider — its auth capabilities, base URL,
19
30
  * default model, and optional live model-discovery hook.
@@ -54,9 +65,57 @@ export interface ProviderProfile {
54
65
  * Alternate names that resolve to this profile. Resolution is
55
66
  * case-insensitive. Alias conflicts with another profile's primary name
56
67
  * cause a `TypeError` at registration time.
68
+ *
69
+ * The canonical alias map (`codex`→`openai`, `google`→`gemini`, …) is declared
70
+ * here as DATA so the resolver never branches on provider name in code
71
+ * (T11756 AC2 · hermes `ProviderProfile.aliases` parity).
57
72
  */
58
73
  aliases?: ReadonlyArray<string>;
59
74
 
75
+ /**
76
+ * Coarse capability tier for system-of-use routing (E9). Sourced from the
77
+ * models.dev catalog capabilities when generated (E8 join), hand-set for
78
+ * builtins. Absent = unknown (the router treats it as `standard`).
79
+ *
80
+ * Hermes derives this from `model_metadata`; CLEO carries it as profile DATA
81
+ * so `resolveLLMForSystem` can pick the right tier without a code branch.
82
+ *
83
+ * @task T11756
84
+ */
85
+ readonly tier?: ProviderTier;
86
+
87
+ /**
88
+ * Cheap/fast auxiliary model for low-stakes calls (title generation, summaries,
89
+ * the warm/aux tier). Hermes `ProviderProfile.default_aux_model` parity. Absent
90
+ * (or empty) → fall back to {@link defaultModel}.
91
+ *
92
+ * @task T11756
93
+ */
94
+ readonly defaultAuxModel?: string;
95
+
96
+ /**
97
+ * Default `maxTokens` for this provider when the caller omits it. Hermes
98
+ * `ProviderProfile.default_max_tokens` parity. Absent → the transport's own
99
+ * default applies.
100
+ *
101
+ * @task T11756
102
+ */
103
+ readonly defaultMaxTokens?: number;
104
+
105
+ /**
106
+ * Temperature handling this provider REQUIRES. Hermes
107
+ * `ProviderProfile.fixed_temperature` parity:
108
+ * - a `number` — the provider only accepts this exact temperature (e.g. the
109
+ * OpenAI o-series require `1`); the transport MUST send it and ignore the
110
+ * caller's override.
111
+ * - `'omit'` — the provider rejects a `temperature` field entirely; the
112
+ * transport MUST NOT send one.
113
+ * - absent — the caller's temperature (or the transport default) applies.
114
+ *
115
+ * @task T11756
116
+ */
117
+ readonly fixedTemperature?: number | 'omit';
118
+
60
119
  /**
61
120
  * HTTP headers sent with every request to this provider.
62
121
  * Merged into the SDK client's `defaultHeaders`.
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Tests for the per-operation OUTPUT contract SSoT (T11692 / DHQ-057).
3
+ *
4
+ * Pins the proof-set of OUTPUT contracts and — critically — encodes the exact
5
+ * shape fact that produced the bug: `tasks.show` returns the task under `task`,
6
+ * so the canonical pointer is `/data/task/title`, NOT `/data/title`.
7
+ *
8
+ * Also validates that mutation contracts reflect the MinimalMutateEnvelope
9
+ * shape (string[] ids, NOT object arrays) and that tasks.find uses the
10
+ * { results, total } wrapper (NOT a bare array).
11
+ *
12
+ * @task T11692
13
+ * @epic T11679
14
+ */
15
+
16
+ import { describe, expect, it } from 'vitest';
17
+ import type { OperationOutputContract } from '../output-contract.js';
18
+ import { OUTPUT_CONTRACTS } from '../output-contracts-data.js';
19
+
20
+ describe('operations/output-contracts (T11692 · DHQ-057)', () => {
21
+ it('registers the high-traffic proof-set operations', () => {
22
+ for (const op of [
23
+ 'tasks.show',
24
+ 'tasks.list',
25
+ 'tasks.find',
26
+ 'tasks.add',
27
+ 'tasks.add-batch',
28
+ 'tasks.update',
29
+ 'tasks.complete',
30
+ ]) {
31
+ expect(OUTPUT_CONTRACTS[op], `missing output contract for ${op}`).toBeDefined();
32
+ }
33
+ });
34
+
35
+ it('keys every entry by its own operation id', () => {
36
+ for (const [key, contract] of Object.entries(OUTPUT_CONTRACTS)) {
37
+ expect(contract.operation).toBe(key);
38
+ }
39
+ });
40
+
41
+ describe('tasks.show — the operation that bit us', () => {
42
+ const show = OUTPUT_CONTRACTS['tasks.show'] as OperationOutputContract;
43
+
44
+ it('encodes that the task is nested under `task` (data schema)', () => {
45
+ const schema = show.dataSchema as {
46
+ required?: string[];
47
+ properties?: Record<string, { properties?: Record<string, unknown> }>;
48
+ };
49
+ expect(schema.required).toContain('task');
50
+ // The title lives under task.properties.title — NOT at the data root.
51
+ expect(schema.properties?.task?.properties).toHaveProperty('title');
52
+ expect(schema.properties).not.toHaveProperty('title');
53
+ });
54
+
55
+ it('lists /data/task/title as a valid --field pointer and NOT /data/title', () => {
56
+ expect(show.fieldPointers).toContain('/data/task/title');
57
+ expect(show.fieldPointers).not.toContain('/data/title');
58
+ });
59
+
60
+ it('carries a shapeNote steering agents away from /data/<field>', () => {
61
+ expect(show.shapeNote).toBeTruthy();
62
+ expect(show.shapeNote).toContain('/data/task/');
63
+ });
64
+
65
+ it('notes that view may be null', () => {
66
+ expect(show.shapeNote).toContain('null');
67
+ });
68
+ });
69
+
70
+ describe('mutation envelopes — MinimalMutateEnvelope (string[] ids, T9931)', () => {
71
+ it('tasks.add surfaces the created task ID as a bare string at /data/created/0', () => {
72
+ const add = OUTPUT_CONTRACTS['tasks.add'] as OperationOutputContract;
73
+ // Correct: /data/created/0 → "T11692" (string), NOT /data/created/0/id
74
+ expect(add.fieldPointers).toContain('/data/created/0');
75
+ expect(add.fieldPointers).not.toContain('/data/created/0/id');
76
+ // Schema must reflect string[] items, not object[]
77
+ const schema = add.dataSchema as {
78
+ required?: string[];
79
+ properties?: Record<string, { items?: { type?: string } }>;
80
+ };
81
+ expect(schema.required).toEqual(
82
+ expect.arrayContaining(['count', 'created', 'updated', 'deleted']),
83
+ );
84
+ expect(schema.properties?.created?.items?.type).toBe('string');
85
+ expect(schema.properties?.updated?.items?.type).toBe('string');
86
+ expect(schema.properties?.deleted?.items?.type).toBe('string');
87
+ });
88
+
89
+ it('tasks.update / tasks.complete surface the changed task ID at /data/updated/0 (string)', () => {
90
+ for (const op of ['tasks.update', 'tasks.complete']) {
91
+ const c = OUTPUT_CONTRACTS[op] as OperationOutputContract;
92
+ // Correct: /data/updated/0 → "T11692" (string), NOT /data/updated/0/id
93
+ expect(c.fieldPointers).toContain('/data/updated/0');
94
+ expect(c.fieldPointers).not.toContain('/data/updated/0/id');
95
+ const schema = c.dataSchema as {
96
+ properties?: Record<string, { items?: { type?: string } }>;
97
+ };
98
+ expect(schema.properties?.updated?.items?.type).toBe('string');
99
+ }
100
+ });
101
+
102
+ it('tasks.add-batch dry-run pointers point to root /data/wouldCreate and /data/insertedCount', () => {
103
+ const batch = OUTPUT_CONTRACTS['tasks.add-batch'] as OperationOutputContract;
104
+ // Correct: root-level, NOT /data/dryRunSummary/wouldCreate
105
+ expect(batch.fieldPointers).toContain('/data/wouldCreate');
106
+ expect(batch.fieldPointers).toContain('/data/insertedCount');
107
+ expect(batch.fieldPointers).not.toContain('/data/dryRunSummary/wouldCreate');
108
+ expect(batch.fieldPointers).not.toContain('/data/dryRunSummary/insertedCount');
109
+ // shapeNote should clarify they are NOT under dryRunSummary
110
+ expect(batch.shapeNote).toContain('/data/wouldCreate');
111
+ });
112
+ });
113
+
114
+ it('tasks.find data is a wrapper object with /data/results (array) and /data/total', () => {
115
+ const find = OUTPUT_CONTRACTS['tasks.find'] as OperationOutputContract;
116
+ // Correct: wrapper object, NOT a bare array
117
+ expect((find.dataSchema as { type?: string }).type).toBe('object');
118
+ // Correct pointers
119
+ expect(find.fieldPointers).toContain('/data/results/0/id');
120
+ expect(find.fieldPointers).toContain('/data/total');
121
+ // Wrong pointers must NOT appear
122
+ expect(find.fieldPointers).not.toContain('/data/0/id');
123
+ expect(find.shapeNote).toContain('/data/results');
124
+ });
125
+ });
@@ -383,15 +383,25 @@ export interface DocsGenerateResult {
383
383
  /**
384
384
  * Parameters for `docs.add`.
385
385
  *
386
- * Exactly one of `file` or `url` must be provided.
386
+ * Exactly one of `file`, `url`, or `content` must be provided.
387
387
  */
388
388
  export interface DocsAddParams {
389
389
  /** Owner entity ID (task, session, observation, etc.) — required. */
390
390
  ownerId: string;
391
- /** Local file path to attach (mutually exclusive with `url`). */
391
+ /** Local file path to attach (mutually exclusive with `url` and `content`). */
392
392
  file?: string;
393
- /** URL to attach as reference (mutually exclusive with `file`). */
393
+ /** URL to attach as reference (mutually exclusive with `file` and `content`). */
394
394
  url?: string;
395
+ /**
396
+ * Inline document body authored directly on the command line or piped via
397
+ * stdin (`--content -`). Mutually exclusive with `file` and `url`. Stored
398
+ * as a `text/markdown` blob exactly as a file-sourced add would be —
399
+ * including the manifest.db mirror, slug reservation, memory observation,
400
+ * and audit trail — so a doc can be created without a pre-existing file.
401
+ *
402
+ * @task T10965
403
+ */
404
+ content?: string;
395
405
  /** Optional human-readable description. */
396
406
  desc?: string;
397
407
  /** Optional comma-separated labels for categorization. */