@dotdrelle/wiki-manager 0.8.2 → 0.10.4

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.
@@ -0,0 +1,302 @@
1
+ const STATUS_VALUES = [
2
+ 'pending',
3
+ 'queued',
4
+ 'running',
5
+ 'waiting',
6
+ 'pending_approval',
7
+ 'done',
8
+ 'failed',
9
+ 'cancelled',
10
+ 'canceled',
11
+ 'stalled',
12
+ 'added_during_run',
13
+ 'error',
14
+ 'complete',
15
+ 'completed',
16
+ 'success',
17
+ ];
18
+
19
+ const nullableString = { type: ['string', 'null'] };
20
+ const nullableObject = { type: ['object', 'null'], additionalProperties: true };
21
+ const outputReferenceSchema = {
22
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/output-reference/v1',
23
+ title: 'OutputReference',
24
+ schemaVersion: '1',
25
+ oneOf: [
26
+ { type: 'string' },
27
+ {
28
+ type: 'object',
29
+ required: ['type', 'ref'],
30
+ additionalProperties: true,
31
+ properties: {
32
+ type: { type: 'string' },
33
+ ref: { type: 'string' },
34
+ label: nullableString,
35
+ workspace: nullableString,
36
+ },
37
+ },
38
+ ],
39
+ };
40
+
41
+ const planStepSchema = {
42
+ type: 'object',
43
+ required: ['description', 'status', 'dependsOn', 'outputRefs'],
44
+ additionalProperties: true,
45
+ properties: {
46
+ step: { type: 'number' },
47
+ id: nullableString,
48
+ description: { type: 'string' },
49
+ status: { type: 'string' },
50
+ dependsOn: { type: 'array', items: { type: 'string' } },
51
+ executor: nullableString,
52
+ executorQuery: nullableObject,
53
+ outputRefs: { type: 'array', items: outputReferenceSchema },
54
+ },
55
+ };
56
+
57
+ const patchTaskSchema = {
58
+ type: 'object',
59
+ required: ['id', 'description'],
60
+ additionalProperties: true,
61
+ properties: {
62
+ id: { type: 'string' },
63
+ description: { type: 'string' },
64
+ status: { type: 'string' },
65
+ dependsOn: { type: 'array', items: { type: 'string' } },
66
+ executor: nullableString,
67
+ executorQuery: nullableObject,
68
+ outputRefs: { type: 'array', items: outputReferenceSchema },
69
+ },
70
+ };
71
+
72
+ export const contractSchemas = {
73
+ activity: {
74
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/activity/v1',
75
+ title: '_activity',
76
+ schemaVersion: '1',
77
+ type: 'object',
78
+ required: ['schemaVersion', 'id', 'source', 'kind', 'label', 'status', 'progress', 'poll', 'outputRefs'],
79
+ additionalProperties: true,
80
+ properties: {
81
+ schemaVersion: { const: '1' },
82
+ id: { type: 'string' },
83
+ source: { type: 'string' },
84
+ kind: { type: 'string' },
85
+ label: { type: 'string' },
86
+ status: { type: 'string' },
87
+ progress: {
88
+ type: 'object',
89
+ additionalProperties: true,
90
+ properties: {
91
+ percent: { type: 'number', minimum: 0, maximum: 100 },
92
+ stepId: { type: 'string' },
93
+ parentActivityKey: { type: 'string' },
94
+ detail: { type: 'string' },
95
+ },
96
+ },
97
+ poll: nullableObject,
98
+ outputRefs: { type: 'array', items: outputReferenceSchema },
99
+ },
100
+ },
101
+ agentRunEvent: {
102
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/agent-run-event/v1',
103
+ title: 'AgentRunEvent',
104
+ schemaVersion: '1',
105
+ type: 'object',
106
+ required: ['id', 'ts', 'type', 'origin', 'payload'],
107
+ additionalProperties: true,
108
+ properties: {
109
+ id: { type: 'string' },
110
+ ts: { type: 'string' },
111
+ type: { type: 'string' },
112
+ origin: { type: 'string' },
113
+ runId: nullableString,
114
+ turnId: nullableString,
115
+ taskId: nullableString,
116
+ workspace: nullableString,
117
+ payload: { type: 'object', additionalProperties: true },
118
+ },
119
+ },
120
+ runRequest: {
121
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/run-request/v1',
122
+ title: 'RuntimeRunRequest',
123
+ schemaVersion: '1',
124
+ type: 'object',
125
+ required: ['input'],
126
+ additionalProperties: true,
127
+ properties: {
128
+ input: { type: 'string', minLength: 1 },
129
+ prompt: { type: 'string' },
130
+ workspace: nullableString,
131
+ runId: nullableString,
132
+ turnId: nullableString,
133
+ },
134
+ },
135
+ controlMessage: {
136
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/control-message/v1',
137
+ title: 'RuntimeControlMessage',
138
+ schemaVersion: '1',
139
+ type: 'object',
140
+ required: ['action'],
141
+ additionalProperties: true,
142
+ properties: {
143
+ action: { type: 'string', enum: ['status', 'explain', 'message', 'enqueue', 'approve_patch', 'reject_patch'] },
144
+ input: { type: 'string' },
145
+ message: { type: 'string' },
146
+ prompt: { type: 'string' },
147
+ request: { type: 'string' },
148
+ intent: { type: 'string', enum: ['observe', 'converse', 'mutate', 'enqueue'] },
149
+ workspace: nullableString,
150
+ patchId: { type: 'string' },
151
+ id: { type: 'string' },
152
+ reason: { type: 'string' },
153
+ },
154
+ },
155
+ plan: {
156
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/plan/v1',
157
+ title: 'StructuredPlan',
158
+ schemaVersion: '1',
159
+ type: 'array',
160
+ items: planStepSchema,
161
+ },
162
+ planPatch: {
163
+ $id: 'https://dotdrelle.dev/wiki-manager/contracts/plan-patch/v1',
164
+ title: 'PlanPatch',
165
+ schemaVersion: '1',
166
+ type: 'object',
167
+ required: ['basePlanRevision', 'operations'],
168
+ additionalProperties: true,
169
+ properties: {
170
+ id: nullableString,
171
+ targetRunId: nullableString,
172
+ basePlanRevision: { type: 'number', minimum: 0 },
173
+ reason: nullableString,
174
+ operations: {
175
+ type: 'array',
176
+ items: {
177
+ type: 'object',
178
+ required: ['op'],
179
+ additionalProperties: true,
180
+ properties: {
181
+ op: { type: 'string', enum: ['add_task', 'add_dependency', 'remove_dependency', 'cancel_task', 'replace_executor', 'request_approval'] },
182
+ task: patchTaskSchema,
183
+ taskId: { type: 'string' },
184
+ targetTaskId: { type: 'string' },
185
+ dependencyId: { type: 'string' },
186
+ dependsOn: { type: 'string' },
187
+ executor: nullableString,
188
+ executorQuery: nullableObject,
189
+ reason: { type: 'string' },
190
+ },
191
+ },
192
+ },
193
+ },
194
+ },
195
+ outputReference: outputReferenceSchema,
196
+ };
197
+
198
+ export function validateContract(name, value) {
199
+ const schema = contractSchemas[name];
200
+ if (!schema) {
201
+ throw new Error(`Unknown contract schema: ${name}`);
202
+ }
203
+ const errors = [];
204
+ validateSchema(schema, value, name, errors);
205
+ return {
206
+ ok: errors.length === 0,
207
+ errors,
208
+ };
209
+ }
210
+
211
+ export function assertContract(name, value) {
212
+ const result = validateContract(name, value);
213
+ if (!result.ok) {
214
+ throw new Error(`Contract ${name} invalid: ${result.errors.join('; ')}`);
215
+ }
216
+ return value;
217
+ }
218
+
219
+ export function validateContractInDev(name, value) {
220
+ if (!contractValidationEnabled()) return value;
221
+ return assertContract(name, value);
222
+ }
223
+
224
+ export function contractValidationEnabled() {
225
+ return process.env.WIKI_MANAGER_VALIDATE_CONTRACTS === '1'
226
+ || process.env.CI === 'true'
227
+ || (process.env.NODE_ENV && process.env.NODE_ENV !== 'production');
228
+ }
229
+
230
+ function validateSchema(schema, value, path, errors) {
231
+ if (schema.oneOf) {
232
+ const matches = schema.oneOf.filter((candidate) => validateCandidate(candidate, value, path));
233
+ if (matches.length !== 1) errors.push(`${path} must match exactly one schema`);
234
+ return;
235
+ }
236
+ if (schema.anyOf) {
237
+ const matches = schema.anyOf.filter((candidate) => validateCandidate(candidate, value, path));
238
+ if (matches.length < 1) errors.push(`${path} must match at least one schema`);
239
+ return;
240
+ }
241
+ if (schema.const !== undefined && value !== schema.const) {
242
+ errors.push(`${path} must equal ${JSON.stringify(schema.const)}`);
243
+ return;
244
+ }
245
+ if (schema.enum && !schema.enum.includes(value)) {
246
+ errors.push(`${path} must be one of ${schema.enum.join(', ')}`);
247
+ return;
248
+ }
249
+ if (schema.type && !typeMatches(schema.type, value)) {
250
+ errors.push(`${path} must be ${formatType(schema.type)}`);
251
+ return;
252
+ }
253
+ if (typeof value === 'string' && schema.minLength != null && value.length < schema.minLength) {
254
+ errors.push(`${path} must have length >= ${schema.minLength}`);
255
+ }
256
+ if (typeof value === 'number') {
257
+ if (schema.minimum != null && value < schema.minimum) errors.push(`${path} must be >= ${schema.minimum}`);
258
+ if (schema.maximum != null && value > schema.maximum) errors.push(`${path} must be <= ${schema.maximum}`);
259
+ }
260
+ if (Array.isArray(value)) {
261
+ value.forEach((item, index) => validateSchema(schema.items ?? {}, item, `${path}[${index}]`, errors));
262
+ return;
263
+ }
264
+ if (value && typeof value === 'object') {
265
+ for (const key of schema.required ?? []) {
266
+ if (!Object.hasOwn(value, key)) errors.push(`${path}.${key} is required`);
267
+ }
268
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
269
+ if (Object.hasOwn(value, key)) validateSchema(childSchema, value[key], `${path}.${key}`, errors);
270
+ }
271
+ if (schema.additionalProperties === false) {
272
+ const allowed = new Set(Object.keys(schema.properties ?? {}));
273
+ for (const key of Object.keys(value)) {
274
+ if (!allowed.has(key)) errors.push(`${path}.${key} is not allowed`);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ function validateCandidate(schema, value, path) {
281
+ const errors = [];
282
+ validateSchema(schema, value, path, errors);
283
+ return errors.length === 0;
284
+ }
285
+
286
+ function typeMatches(type, value) {
287
+ const types = Array.isArray(type) ? type : [type];
288
+ return types.some((candidate) => {
289
+ if (candidate === 'array') return Array.isArray(value);
290
+ if (candidate === 'null') return value === null;
291
+ if (candidate === 'integer') return Number.isInteger(value);
292
+ if (candidate === 'number') return typeof value === 'number' && Number.isFinite(value);
293
+ if (candidate === 'object') return value !== null && typeof value === 'object' && !Array.isArray(value);
294
+ return typeof value === candidate;
295
+ });
296
+ }
297
+
298
+ function formatType(type) {
299
+ return Array.isArray(type) ? type.join('|') : type;
300
+ }
301
+
302
+ export { STATUS_VALUES };
@@ -0,0 +1,93 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { assertContract, validateContract } from './schemas.js';
4
+
5
+ test('activity contract accepts the canonical v1 shape and extra fields', () => {
6
+ const activity = {
7
+ schemaVersion: '1',
8
+ id: 'job-1',
9
+ source: 'production',
10
+ kind: 'build',
11
+ label: 'Production build',
12
+ status: 'running',
13
+ progress: {
14
+ percent: 42,
15
+ stepId: 'draft',
16
+ parentActivityKey: 'production:parent',
17
+ detail: 'Drafting',
18
+ },
19
+ poll: null,
20
+ outputRefs: ['deliverables/report.md', { type: 'wiki_page', ref: 'Reports/Build' }],
21
+ agentSpecificField: true,
22
+ };
23
+
24
+ assert.equal(validateContract('activity', activity).ok, true);
25
+ assert.equal(assertContract('activity', activity), activity);
26
+ });
27
+
28
+ test('activity contract rejects missing canonical fields', () => {
29
+ const result = validateContract('activity', {
30
+ id: 'job-1',
31
+ source: 'production',
32
+ kind: 'build',
33
+ label: 'Production build',
34
+ status: 'running',
35
+ progress: {},
36
+ poll: null,
37
+ outputRefs: [],
38
+ });
39
+
40
+ assert.equal(result.ok, false);
41
+ assert.ok(result.errors.some((error) => error.includes('schemaVersion')));
42
+ });
43
+
44
+ test('agent event contract carries the unified audit identity fields', () => {
45
+ const event = {
46
+ id: 'event-1',
47
+ ts: new Date(0).toISOString(),
48
+ type: 'tool_call_result',
49
+ origin: 'runtime',
50
+ runId: 'run-1',
51
+ turnId: 'turn-1',
52
+ taskId: 'task-1',
53
+ workspace: 'juno',
54
+ payload: { toolCallId: 'call-1', ok: true },
55
+ };
56
+
57
+ assert.equal(validateContract('agentRunEvent', event).ok, true);
58
+ });
59
+
60
+ test('run and control request contracts reject empty run input but accept explicit controls', () => {
61
+ assert.equal(validateContract('runRequest', { input: 'Build docs', workspace: 'juno' }).ok, true);
62
+ assert.equal(validateContract('runRequest', { input: '' }).ok, false);
63
+ assert.equal(validateContract('controlMessage', { action: 'message', input: 'Ou en est le build ?', intent: 'observe' }).ok, true);
64
+ });
65
+
66
+ test('plan and patch contracts cover structured dependencies and output refs', () => {
67
+ const plan = [{
68
+ step: 1,
69
+ id: 'task-a',
70
+ description: 'Export',
71
+ status: 'pending',
72
+ dependsOn: [],
73
+ executor: 'cme.cme_export_run',
74
+ executorQuery: null,
75
+ outputRefs: ['raw/export.json'],
76
+ }];
77
+ const patch = {
78
+ targetRunId: 'run-1',
79
+ basePlanRevision: 4,
80
+ operations: [{
81
+ op: 'add_task',
82
+ task: {
83
+ id: 'task-b',
84
+ description: 'Build',
85
+ dependsOn: ['task-a'],
86
+ executorQuery: { capability: 'production build' },
87
+ },
88
+ }],
89
+ };
90
+
91
+ assert.equal(validateContract('plan', plan).ok, true);
92
+ assert.equal(validateContract('planPatch', patch).ok, true);
93
+ });
@@ -1,3 +1,5 @@
1
+ import { validateContractInDev } from '../contracts/schemas.js';
2
+
1
3
  export function parseJsonText(text) {
2
4
  try {
3
5
  return JSON.parse(text);
@@ -14,7 +16,7 @@ function terminalStatus(status) {
14
16
  return ['done', 'failed', 'cancelled', 'canceled', 'complete', 'completed', 'success', 'error'].includes(String(status ?? '').toLowerCase());
15
17
  }
16
18
 
17
- function activityKey(activity) {
19
+ export function activityKey(activity) {
18
20
  const id = activity?.id ?? activity?.jobId ?? activity?.job_id;
19
21
  const source = activity?.source ?? activity?.agent ?? activity?.poll?.server ?? 'mcp';
20
22
  return `${source}:${id ?? activity?.kind ?? activity?.label ?? 'activity'}`;
@@ -25,6 +27,10 @@ function normalizePlanSteps(steps) {
25
27
  return steps.map((s, i) => ({
26
28
  id: s != null && s.id != null ? String(s.id) : String(i + 1),
27
29
  label: s != null ? String(s.label ?? s.description ?? s.name ?? s.id ?? (i + 1)) : String(i + 1),
30
+ dependsOn: Array.isArray(s?.dependsOn) ? s.dependsOn.map(String) : [],
31
+ executor: s?.executor ?? null,
32
+ executorQuery: s?.executorQuery ?? null,
33
+ outputRefs: Array.isArray(s?.outputRefs) ? s.outputRefs.map(String) : [],
28
34
  }));
29
35
  }
30
36
 
@@ -62,9 +68,13 @@ export function normalizeActivity(activity, fallback = {}) {
62
68
  kind,
63
69
  step,
64
70
  ].filter(Boolean).join(' ');
71
+ const outputRefs = Array.isArray(activity.outputRefs)
72
+ ? activity.outputRefs.map((ref) => (ref && typeof ref === 'object' ? { ...ref } : String(ref)))
73
+ : [];
65
74
  const normalized = {
66
75
  key: null,
67
- id,
76
+ schemaVersion: String(activity.schemaVersion ?? '1'),
77
+ id: String(id ?? kind ?? 'activity'),
68
78
  source: String(source),
69
79
  kind: String(kind),
70
80
  label: String(label || `${source} ${kind}`),
@@ -79,12 +89,14 @@ export function normalizeActivity(activity, fallback = {}) {
79
89
  },
80
90
  plan: planSteps ? { steps: planSteps } : null,
81
91
  poll: normalizePoll(activity.poll ?? fallback.poll),
92
+ outputRefs,
82
93
  startedAt: activity.startedAt ?? fallback.startedAt ?? null,
83
94
  updatedAt: activity.updatedAt ?? new Date().toISOString(),
84
95
  error: activity.error ?? null,
85
96
  terminal: Boolean(activity.terminal ?? terminalStatus(status)),
86
97
  };
87
98
  normalized.key = activityKey(normalized);
99
+ validateContractInDev('activity', normalized);
88
100
  return normalized;
89
101
  }
90
102
 
@@ -11,9 +11,11 @@ test('normalizeActivity: plan.steps preserved with id and label', () => {
11
11
  plan: { steps: [{ id: 'extract', label: 'Extraction' }, { id: 'build', label: 'Build' }] },
12
12
  progress: {},
13
13
  });
14
+ assert.equal(a.schemaVersion, '1');
15
+ assert.deepEqual(a.outputRefs, []);
14
16
  assert.deepEqual(a.plan.steps, [
15
- { id: 'extract', label: 'Extraction' },
16
- { id: 'build', label: 'Build' },
17
+ { id: 'extract', label: 'Extraction', dependsOn: [], executor: null, executorQuery: null, outputRefs: [] },
18
+ { id: 'build', label: 'Build', dependsOn: [], executor: null, executorQuery: null, outputRefs: [] },
17
19
  ]);
18
20
  });
19
21