@jagilber-org/index-server 1.27.2 → 1.28.1

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 (45) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/CONTRIBUTING.md +3 -3
  3. package/dist/dashboard/client/admin.html +28 -28
  4. package/dist/dashboard/client/js/admin.feedback.js +1 -1
  5. package/dist/dashboard/client/js/admin.instructions.js +1 -1
  6. package/dist/dashboard/security/SecurityMonitor.js +2 -2
  7. package/dist/dashboard/server/AdminPanelState.js +5 -1
  8. package/dist/dashboard/server/ApiRoutes.js +2 -1
  9. package/dist/dashboard/server/MetricsCollector.js +3 -2
  10. package/dist/dashboard/server/WebSocketManager.js +2 -2
  11. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  12. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +1 -1
  13. package/dist/dashboard/server/routes/api.usage.routes.js +5 -1
  14. package/dist/dashboard/server/routes/instructions.routes.js +142 -12
  15. package/dist/dashboard/server/routes/scripts.routes.js +1 -1
  16. package/dist/dashboard/server/routes/sqlite.routes.js +74 -0
  17. package/dist/models/instruction.d.ts +1 -1
  18. package/dist/schemas/index.d.ts +1 -1
  19. package/dist/schemas/index.js +1 -1
  20. package/dist/server/index-server.js +1 -1
  21. package/dist/services/auditLog.d.ts +1 -1
  22. package/dist/services/auditLog.js +1 -1
  23. package/dist/services/feedbackStorage.js +1 -1
  24. package/dist/services/handlers/instructions.add.js +36 -3
  25. package/dist/services/handlers.feedback.js +1 -1
  26. package/dist/services/handlers.instructionSchema.js +4 -4
  27. package/dist/services/handlers.search.js +3 -3
  28. package/dist/services/instructionRecordValidation.d.ts +3 -0
  29. package/dist/services/instructionRecordValidation.js +64 -4
  30. package/dist/services/seedBootstrap.js +2 -2
  31. package/dist/services/toolRegistry.js +8 -8
  32. package/dist/services/toolRegistry.zod.js +3 -3
  33. package/dist/versioning/schemaVersion.d.ts +1 -1
  34. package/dist/versioning/schemaVersion.js +47 -2
  35. package/package.json +44 -40
  36. package/schemas/index-server.code-schema.json +1 -1
  37. package/schemas/instruction.schema.json +3 -3
  38. package/schemas/json-schema/instruction-content-type.schema.json +1 -1
  39. package/schemas/json-schema/instruction-instruction-entry.schema.json +1 -1
  40. package/scripts/README.md +48 -0
  41. package/scripts/{generate-certs.mjs → build/generate-certs.mjs} +1 -1
  42. package/scripts/{setup-wizard.mjs → build/setup-wizard.mjs} +1 -1
  43. package/scripts/{setup-hooks.cjs → hooks/setup-hooks.cjs} +3 -3
  44. package/server.json +2 -2
  45. /package/scripts/{copy-dashboard-assets.mjs → build/copy-dashboard-assets.mjs} +0 -0
@@ -566,5 +566,79 @@ function createSqliteRoutes() {
566
566
  return res.status(code).json({ success: false, error: err.message });
567
567
  }
568
568
  });
569
+ // ── Validation ────────────────────────────────────────────────────────
570
+ /** GET /sqlite/validate — Comprehensive database validation (read-only) */
571
+ router.get('/sqlite/validate', adminAuth_js_1.dashboardAdminAuth, (_req, res) => {
572
+ try {
573
+ assertSqliteActive();
574
+ const sqlitePath = getSqlitePath();
575
+ const db = new node_sqlite_1.DatabaseSync(sqlitePath, { readOnly: true });
576
+ const checks = [];
577
+ try {
578
+ // 1. Integrity check
579
+ const intRows = db.prepare('PRAGMA integrity_check').all();
580
+ const intOk = intRows.length === 1 && intRows[0].integrity_check === 'ok';
581
+ checks.push({ name: 'integrity_check', pass: intOk, detail: intOk ? 'ok' : JSON.stringify(intRows) });
582
+ // 2. Table counts
583
+ const instrCount = db.prepare('SELECT COUNT(*) as cnt FROM instructions').get()?.cnt ?? 0;
584
+ checks.push({ name: 'instructions_exist', pass: instrCount >= 0, detail: `count=${instrCount}` });
585
+ // 3. FTS5 sync
586
+ let ftsCount = -1;
587
+ try {
588
+ ftsCount = db.prepare('SELECT COUNT(*) as cnt FROM instructions_fts').get()?.cnt ?? -1;
589
+ checks.push({ name: 'fts5_sync', pass: ftsCount === instrCount, detail: `instructions=${instrCount}, fts=${ftsCount}` });
590
+ }
591
+ catch {
592
+ checks.push({ name: 'fts5_sync', pass: false, detail: 'FTS5 table not accessible' });
593
+ }
594
+ // 4. Orphaned usage records
595
+ try {
596
+ const orphanUsage = db.prepare('SELECT COUNT(*) as cnt FROM usage WHERE instruction_id NOT IN (SELECT id FROM instructions)').get()?.cnt ?? 0;
597
+ checks.push({ name: 'usage_no_orphans', pass: orphanUsage === 0, detail: `orphans=${orphanUsage}` });
598
+ }
599
+ catch {
600
+ checks.push({ name: 'usage_no_orphans', pass: true, detail: 'usage table not present (ok)' });
601
+ }
602
+ // 5. Embedding consistency
603
+ try {
604
+ const embCount = db.prepare('SELECT COUNT(*) as cnt FROM embeddings').get()?.cnt ?? 0;
605
+ const embMetaCount = db.prepare('SELECT COUNT(*) as cnt FROM embedding_meta').get()?.cnt ?? 0;
606
+ checks.push({ name: 'embedding_meta_sync', pass: embCount === embMetaCount, detail: `embeddings=${embCount}, meta=${embMetaCount}` });
607
+ const orphanEmb = db.prepare('SELECT COUNT(*) as cnt FROM embedding_meta WHERE instruction_id NOT IN (SELECT id FROM instructions)').get()?.cnt ?? 0;
608
+ checks.push({ name: 'embedding_no_orphans', pass: orphanEmb === 0, detail: `orphans=${orphanEmb}` });
609
+ }
610
+ catch {
611
+ checks.push({ name: 'embedding_meta_sync', pass: true, detail: 'embedding tables not present (ok)' });
612
+ }
613
+ // 6. NULL required fields
614
+ const nullIds = db.prepare("SELECT COUNT(*) as cnt FROM instructions WHERE id IS NULL OR id = ''").get()?.cnt ?? 0;
615
+ checks.push({ name: 'no_null_ids', pass: nullIds === 0, detail: `count=${nullIds}` });
616
+ const nullTitles = db.prepare("SELECT COUNT(*) as cnt FROM instructions WHERE title IS NULL OR title = ''").get()?.cnt ?? 0;
617
+ checks.push({ name: 'no_null_titles', pass: nullTitles === 0, detail: `count=${nullTitles}` });
618
+ const nullBodies = db.prepare("SELECT COUNT(*) as cnt FROM instructions WHERE body IS NULL OR body = ''").get()?.cnt ?? 0;
619
+ checks.push({ name: 'no_null_bodies', pass: nullBodies === 0, detail: `count=${nullBodies}` });
620
+ // 7. WAL mode
621
+ const jm = db.prepare('PRAGMA journal_mode').get();
622
+ const isWal = jm?.journal_mode === 'wal';
623
+ checks.push({ name: 'wal_mode', pass: isWal, detail: `journal_mode=${jm?.journal_mode ?? 'unknown'}` });
624
+ const allPass = checks.every(c => c.pass);
625
+ return res.json({
626
+ success: true,
627
+ valid: allPass,
628
+ totalChecks: checks.length,
629
+ passed: checks.filter(c => c.pass).length,
630
+ failed: checks.filter(c => !c.pass).length,
631
+ checks,
632
+ });
633
+ }
634
+ finally {
635
+ db.close();
636
+ }
637
+ }
638
+ catch (err) {
639
+ const code = err.statusCode ?? 500;
640
+ return res.status(code).json({ success: false, error: err.message });
641
+ }
642
+ });
569
643
  return router;
570
644
  }
@@ -1,6 +1,6 @@
1
1
  export type AudienceScope = 'individual' | 'group' | 'all';
2
2
  export type RequirementLevel = 'mandatory' | 'critical' | 'recommended' | 'optional' | 'deprecated';
3
- export type ContentType = 'instruction' | 'template' | 'chat-session' | 'reference' | 'example' | 'agent';
3
+ export type ContentType = 'instruction' | 'template' | 'workflow' | 'reference' | 'example' | 'agent';
4
4
  export interface InstructionEntry {
5
5
  id: string;
6
6
  title: string;
@@ -98,7 +98,7 @@ export declare const instructionEntry: {
98
98
  };
99
99
  };
100
100
  readonly contentType: {
101
- readonly enum: readonly ["instruction", "template", "chat-session", "reference", "example", "agent"];
101
+ readonly enum: readonly ["instruction", "template", "workflow", "reference", "example", "agent"];
102
102
  };
103
103
  readonly version: {
104
104
  readonly type: "string";
@@ -44,7 +44,7 @@ exports.instructionEntry = {
44
44
  workspaceId: { type: 'string' },
45
45
  userId: { type: 'string' },
46
46
  teamIds: { type: 'array', items: { type: 'string' } },
47
- contentType: { enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'] },
47
+ contentType: { enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'] },
48
48
  version: { type: 'string' },
49
49
  status: { enum: ['draft', 'review', 'approved', 'deprecated'] },
50
50
  owner: { type: 'string' },
@@ -365,7 +365,7 @@ function parseArgs(argv) {
365
365
  return config;
366
366
  }
367
367
  function launchSetupWizard(argv) {
368
- const wizardPath = path_1.default.join(__dirname, '..', '..', 'scripts', 'setup-wizard.mjs');
368
+ const wizardPath = path_1.default.join(__dirname, '..', '..', 'scripts', 'build', 'setup-wizard.mjs');
369
369
  // Forward all args after --setup/--configure to the wizard
370
370
  const setupIdx = argv.findIndex(a => a === '--setup' || a === '--configure');
371
371
  const forwardArgs = setupIdx >= 0 ? argv.slice(setupIdx + 1) : [];
@@ -4,7 +4,7 @@ export declare function runWithCorrelation<T>(correlationId: string, fn: () => T
4
4
  export declare function getCurrentCorrelationId(): string | undefined;
5
5
  /** Reset the cached audit log path, forcing re-resolution on next write. */
6
6
  export declare function resetAuditLogCache(): void;
7
- export type AuditKind = 'mutation' | 'read' | 'http';
7
+ export type AuditKind = 'mutation' | 'read' | 'http' | 'feedback';
8
8
  export interface AuditEntry {
9
9
  ts: string;
10
10
  kind: AuditKind;
@@ -19,7 +19,7 @@ const logger_1 = require("./logger");
19
19
  const toolRegistry_1 = require("./toolRegistry");
20
20
  // Append-only JSONL audit log for all server operations.
21
21
  // Each line: { ts, kind, action, ids?, meta? }
22
- // kind: 'mutation' | 'read' | 'http' — classifies the entry type.
22
+ // kind: 'mutation' | 'read' | 'http' | 'feedback' — classifies the entry type.
23
23
  // Path and enablement are driven by runtime configuration (instructions.auditLog).
24
24
  // AsyncLocalStorage carries the correlation ID from registry wrapper into handler scope.
25
25
  // This lets logAudit() calls inside handlers automatically include correlationId
@@ -83,6 +83,6 @@ function saveFeedbackStorage(storage) {
83
83
  }
84
84
  function generateFeedbackId(type, timestamp) {
85
85
  const hash = (0, crypto_1.createHash)('sha256');
86
- hash.update(`${type}-${timestamp}-${Math.random()}`);
86
+ hash.update(`${type}-${timestamp}-${(0, crypto_1.randomBytes)(8).toString('hex')}`);
87
87
  return hash.digest('hex').substring(0, 16);
88
88
  }
@@ -96,6 +96,22 @@ const instructions_shared_1 = require("./instructions.shared");
96
96
  return { error: priorLoad };
97
97
  return { error: { code: 'unknown', detail: 'missing-existing-entry', raw: 'missing-existing-entry' } };
98
98
  };
99
+ const validateExistingCollisionFile = (filePath) => {
100
+ if (!fs_1.default.existsSync(filePath))
101
+ return [];
102
+ try {
103
+ const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
104
+ const validationCandidate = { ...raw, schemaVersion: schemaVersion_1.SCHEMA_VERSION };
105
+ const validation = (0, instructionRecordValidation_1.validateInstructionRecord)(validationCandidate);
106
+ return validation.validationErrors
107
+ .filter((issue) => issue.includes('/classification'))
108
+ .map((issue) => (0, instructionRecordValidation_1.sanitizeErrorDetail)(issue) || 'existing entry failed validation');
109
+ }
110
+ catch (err) {
111
+ const sanitized = (0, instructionRecordValidation_1.sanitizeLoadError)(err, 'parse_failed');
112
+ return [sanitized.detail];
113
+ }
114
+ };
99
115
  const verifyReadBack = async (id, filePath, requestedCategories) => {
100
116
  try {
101
117
  (0, indexContext_1.invalidate)();
@@ -222,6 +238,12 @@ const instructions_shared_1 = require("./instructions.shared");
222
238
  mutable.requirement = 'optional';
223
239
  if (mutable.categories === undefined)
224
240
  mutable.categories = [];
241
+ if (mutable.contentType === undefined)
242
+ mutable.contentType = 'instruction';
243
+ }
244
+ const surfaceValidation = (0, instructionRecordValidation_1.validateInstructionInputSurface)(e);
245
+ if (surfaceValidation.validationErrors.length) {
246
+ return failValidation('invalid_instruction', surfaceValidation.validationErrors, surfaceValidation.hints, { id: e.id });
225
247
  }
226
248
  const dir = (0, indexContext_1.getInstructionsDir)();
227
249
  if (!fs_1.default.existsSync(dir))
@@ -257,9 +279,8 @@ const instructions_shared_1 = require("./instructions.shared");
257
279
  e.title === undefined ? 'title: missing required field' : undefined,
258
280
  e.body === undefined ? 'body: missing required field' : undefined,
259
281
  ].filter((issue) => !!issue);
260
- const surfaceValidation = (0, instructionRecordValidation_1.validateInstructionInputSurface)(e);
261
- if (requiredFieldErrors.length || surfaceValidation.validationErrors.length) {
262
- return failValidation(requiredFieldErrors.length ? 'missing required fields' : 'invalid_instruction', [...requiredFieldErrors, ...surfaceValidation.validationErrors], surfaceValidation.hints, { id: e.id });
282
+ if (requiredFieldErrors.length) {
283
+ return failValidation('missing required fields', requiredFieldErrors, surfaceValidation.hints, { id: e.id });
263
284
  }
264
285
  const overwrite = !!p.overwrite;
265
286
  const exists = overwrite ? ((await (0, indexContext_1.ensureLoadedAsync)()).byId.has(e.id) || fs_1.default.existsSync(file)) : false;
@@ -503,6 +524,18 @@ const instructions_shared_1 = require("./instructions.shared");
503
524
  return failValidation('invalid_instruction', err.validationErrors, err.hints, { id: e.id });
504
525
  }
505
526
  if (!overwrite && (0, indexContext_1.isDuplicateInstructionWriteError)(err)) {
527
+ const existingValidationErrors = validateExistingCollisionFile(file);
528
+ if (existingValidationErrors.length) {
529
+ return {
530
+ id: e.id,
531
+ success: false,
532
+ skipped: false,
533
+ created: false,
534
+ overwritten: false,
535
+ error: 'existing_instruction_invalid',
536
+ validationErrors: existingValidationErrors,
537
+ };
538
+ }
506
539
  let st0 = await (0, indexContext_1.ensureLoadedAsync)();
507
540
  let visible = st0.byId.has(e.id);
508
541
  let repaired = false;
@@ -43,7 +43,7 @@ const feedbackStorage_1 = require("./feedbackStorage");
43
43
  type: entry.type,
44
44
  severity: entry.severity,
45
45
  title: entry.title
46
- });
46
+ }, 'feedback');
47
47
  (0, logger_1.logInfo)('[feedback] Feedback submitted', {
48
48
  id: entry.id,
49
49
  type: entry.type,
@@ -71,7 +71,7 @@ function buildMinimalExample() {
71
71
  categories: ["example", "documentation"],
72
72
  primaryCategory: "example",
73
73
  contentType: "instruction",
74
- schemaVersion: "4"
74
+ schemaVersion: "5"
75
75
  };
76
76
  }
77
77
  /**
@@ -89,7 +89,7 @@ function _buildComprehensiveExample() {
89
89
  categories: ["example", "documentation", "governance"],
90
90
  primaryCategory: "governance",
91
91
  contentType: "instruction",
92
- schemaVersion: "4",
92
+ schemaVersion: "5",
93
93
  version: "1.0.0",
94
94
  status: "approved",
95
95
  owner: "platform-team",
@@ -170,8 +170,8 @@ function defineValidationRules() {
170
170
  { field: 'requirement', rule: 'Enum', constraint: 'One of: mandatory, critical, recommended, optional, deprecated' },
171
171
  { field: 'categories', rule: 'Array', constraint: '0-25 items, each 1-49 chars, lowercase, pattern: ^[a-z0-9][a-z0-9-_]{0,48}$' },
172
172
  { field: 'primaryCategory', rule: 'Reference', constraint: 'Must be a member of categories array if present' },
173
- { field: 'contentType', rule: 'Enum', constraint: 'One of: instruction (default), template, chat-session, reference, example, agent' },
174
- { field: 'schemaVersion', rule: 'Enum', constraint: 'Currently "4"' },
173
+ { field: 'contentType', rule: 'Enum', constraint: 'One of: instruction (default), template, workflow, reference, example, agent' },
174
+ { field: 'schemaVersion', rule: 'Enum', constraint: 'Currently "5"' },
175
175
  { field: 'sourceHash', rule: 'Pattern', constraint: 'SHA256 hex string (64 chars) when present' },
176
176
  { field: 'version', rule: 'Pattern', constraint: 'Semantic version: ^\\d+\\.\\d+\\.\\d+$ (e.g., "1.0.0")' },
177
177
  { field: 'status', rule: 'Enum', constraint: 'One of: draft, review, approved, deprecated' },
@@ -41,7 +41,7 @@ const SEARCH_SCHEMA = {
41
41
  limit: { type: 'number', minimum: 1, maximum: 100, default: 50 },
42
42
  includeCategories: { type: 'boolean', default: false },
43
43
  caseSensitive: { type: 'boolean', default: false },
44
- contentType: { type: 'string', enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'] }
44
+ contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'] }
45
45
  },
46
46
  example: { keywords: ['build', 'validate', 'discipline'], limit: 10, includeCategories: true }
47
47
  };
@@ -394,7 +394,7 @@ function performSearch(params) {
394
394
  const caseSensitive = params.caseSensitive ?? false;
395
395
  const contentType = params.contentType;
396
396
  // Validate contentType if provided
397
- const validContentTypes = ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'];
397
+ const validContentTypes = ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'];
398
398
  if (contentType && !validContentTypes.includes(contentType)) {
399
399
  throw new Error(`Invalid contentType: must be one of ${validContentTypes.join(', ')}`);
400
400
  }
@@ -556,7 +556,7 @@ async function handleInstructionsSearch(params) {
556
556
  if (typeof params.contentType !== 'string') {
557
557
  throw new Error('contentType must be a string');
558
558
  }
559
- const validContentTypes = ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'];
559
+ const validContentTypes = ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'];
560
560
  if (!validContentTypes.includes(params.contentType)) {
561
561
  throw new Error(`contentType must be one of: ${validContentTypes.join(', ')}`);
562
562
  }
@@ -14,6 +14,9 @@ export declare class InstructionValidationError extends Error {
14
14
  constructor(validationErrors: string[], hints?: string[], schemaRef?: string);
15
15
  }
16
16
  export declare const INSTRUCTION_ID_MAX_LENGTH = 120;
17
+ export declare const INSTRUCTION_ID_PATTERN: RegExp;
18
+ export declare function validateInstructionIdSurface(id: unknown): string[];
19
+ export declare function validateInstructionInputEnumMembership(entry: Record<string, unknown>): string[];
17
20
  export declare function validateInstructionInputSurface(entry: Record<string, unknown>): InstructionValidationResult;
18
21
  export declare function validateInstructionRecord(entry: InstructionEntry): InstructionValidationResult;
19
22
  export declare function assertValidInstructionRecord(entry: InstructionEntry): InstructionEntry;
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.INSTRUCTION_INPUT_SCHEMA_REF = exports.INSTRUCTION_ID_MAX_LENGTH = exports.InstructionValidationError = void 0;
6
+ exports.INSTRUCTION_INPUT_SCHEMA_REF = exports.INSTRUCTION_ID_PATTERN = exports.INSTRUCTION_ID_MAX_LENGTH = exports.InstructionValidationError = void 0;
7
+ exports.validateInstructionIdSurface = validateInstructionIdSurface;
8
+ exports.validateInstructionInputEnumMembership = validateInstructionInputEnumMembership;
7
9
  exports.validateInstructionInputSurface = validateInstructionInputSurface;
8
10
  exports.validateInstructionRecord = validateInstructionRecord;
9
11
  exports.assertValidInstructionRecord = assertValidInstructionRecord;
@@ -218,12 +220,16 @@ function applyWriteCompatibility(entry) {
218
220
  else if (legacyRequirementMap[upper])
219
221
  next.requirement = legacyRequirementMap[upper];
220
222
  }
223
+ if (next.contentType === 'chat-session') {
224
+ next.contentType = 'workflow';
225
+ }
221
226
  if (typeof next.priority !== 'number' || next.priority < 1 || next.priority > 100)
222
227
  next.priority = 50;
223
228
  return next;
224
229
  }
225
230
  // Matches the upper bound declared in schemas/instruction.schema.json (id maxLength).
226
231
  exports.INSTRUCTION_ID_MAX_LENGTH = 120;
232
+ exports.INSTRUCTION_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-_]{0,118}[a-z0-9])?$/;
227
233
  function findIllegalControlChar(id) {
228
234
  for (let i = 0; i < id.length; i++) {
229
235
  const code = id.charCodeAt(i);
@@ -235,7 +241,7 @@ function findIllegalControlChar(id) {
235
241
  }
236
242
  return undefined;
237
243
  }
238
- function validateIdSurface(id) {
244
+ function validateInstructionIdSurface(id) {
239
245
  if (typeof id !== 'string' || !id.trim())
240
246
  return ['id: missing required field'];
241
247
  const illegal = findIllegalControlChar(id);
@@ -248,8 +254,48 @@ function validateIdSurface(id) {
248
254
  if (id.includes('..') || id.includes('/') || id.includes('\\') || /[:*?"<>|]/.test(id)) {
249
255
  return ['id: must be a safe instruction id without path traversal or path separators'];
250
256
  }
257
+ if (!exports.INSTRUCTION_ID_PATTERN.test(id)) {
258
+ return ['id: must match /^[a-z0-9](?:[a-z0-9-_]{0,118}[a-z0-9])?$/ using lower-case ASCII letters, digits, hyphen, or underscore'];
259
+ }
251
260
  return [];
252
261
  }
262
+ // Enum membership checks for governance fields. These run at input-surface time so that
263
+ // invalid enum values are rejected early, before applyWriteCompatibility can silently
264
+ // coerce them (e.g. status:"active" → "approved", audience:"agents" → "group").
265
+ // However, values that ARE coercible by applyWriteCompatibility are accepted here to
266
+ // avoid rejecting inputs that would otherwise succeed after coercion.
267
+ const VALID_STATUS = ['approved', 'draft', 'review', 'deprecated'];
268
+ const COERCIBLE_STATUS = ['active'];
269
+ const VALID_PRIORITY_TIER = ['P1', 'P2', 'P3', 'P4'];
270
+ const VALID_CLASSIFICATION = ['public', 'internal', 'restricted'];
271
+ const VALID_CONTENT_TYPE = ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'];
272
+ const COERCIBLE_CONTENT_TYPE = ['chat-session'];
273
+ const VALID_AUDIENCE = ['individual', 'group', 'all'];
274
+ const COERCIBLE_AUDIENCE = ['system', 'developers', 'developer', 'team', 'teams', 'users', 'dev', 'devs', 'testers', 'administrators', 'admins', 'agents', 'powershell script authors'];
275
+ const VALID_REQUIREMENT = ['mandatory', 'critical', 'recommended', 'optional', 'deprecated'];
276
+ const COERCIBLE_REQUIREMENT = ['MUST', 'SHOULD', 'MAY', 'CRITICAL', 'OPTIONAL', 'MANDATORY', 'DEPRECATED'];
277
+ function validateInstructionInputEnumMembership(entry) {
278
+ const errs = [];
279
+ if (typeof entry.status === 'string' && !VALID_STATUS.includes(entry.status) && !COERCIBLE_STATUS.includes(entry.status)) {
280
+ errs.push(`/status: must be one of ${VALID_STATUS.join(', ')}`);
281
+ }
282
+ if (typeof entry.priorityTier === 'string' && !VALID_PRIORITY_TIER.includes(entry.priorityTier)) {
283
+ errs.push(`/priorityTier: must be one of ${VALID_PRIORITY_TIER.join(', ')}`);
284
+ }
285
+ if (typeof entry.classification === 'string' && !VALID_CLASSIFICATION.includes(entry.classification)) {
286
+ errs.push(`/classification: must be one of ${VALID_CLASSIFICATION.join(', ')}`);
287
+ }
288
+ if (typeof entry.contentType === 'string' && !VALID_CONTENT_TYPE.includes(entry.contentType) && !COERCIBLE_CONTENT_TYPE.includes(entry.contentType)) {
289
+ errs.push(`/contentType: must be one of ${VALID_CONTENT_TYPE.join(', ')}`);
290
+ }
291
+ if (typeof entry.audience === 'string' && !VALID_AUDIENCE.includes(entry.audience) && !COERCIBLE_AUDIENCE.includes(entry.audience.toLowerCase())) {
292
+ errs.push(`/audience: must be one of ${VALID_AUDIENCE.join(', ')}`);
293
+ }
294
+ if (typeof entry.requirement === 'string' && !VALID_REQUIREMENT.includes(entry.requirement) && !COERCIBLE_REQUIREMENT.includes(entry.requirement.toUpperCase())) {
295
+ errs.push(`/requirement: must be one of ${VALID_REQUIREMENT.join(', ')}`);
296
+ }
297
+ return errs;
298
+ }
253
299
  // Typed-field shape checks. These run for both strict and lax callers so that lax mode
254
300
  // fills defaults for *missing* fields but never silently coerces wrong-typed inputs.
255
301
  function validateTypedInputShape(entry) {
@@ -257,9 +303,22 @@ function validateTypedInputShape(entry) {
257
303
  if (entry.priority !== undefined && typeof entry.priority !== 'number') {
258
304
  errs.push(`/priority: must be a number, received ${typeof entry.priority}`);
259
305
  }
306
+ else if (typeof entry.priority === 'number' && (!Number.isInteger(entry.priority) || entry.priority < 1 || entry.priority > 100)) {
307
+ errs.push('/priority: must be an integer from 1 to 100');
308
+ }
260
309
  if (entry.categories !== undefined && !Array.isArray(entry.categories)) {
261
310
  errs.push(`/categories: must be an array of strings, received ${typeof entry.categories}`);
262
311
  }
312
+ else if (Array.isArray(entry.categories)) {
313
+ for (const [index, category] of entry.categories.entries()) {
314
+ if (typeof category !== 'string') {
315
+ errs.push(`/categories/${index}: must be a string, received ${typeof category}`);
316
+ }
317
+ else if (!/^[a-z0-9][a-z0-9-_]{0,48}$/.test(category.toLowerCase())) {
318
+ errs.push(`/categories/${index}: must match /^[a-z0-9][a-z0-9-_]{0,48}$/`);
319
+ }
320
+ }
321
+ }
263
322
  if (entry.audience !== undefined && typeof entry.audience !== 'string') {
264
323
  errs.push(`/audience: must be a string, received ${typeof entry.audience}`);
265
324
  }
@@ -282,7 +341,7 @@ function validateTypedInputShape(entry) {
282
341
  }
283
342
  function validateInstructionInputSurface(entry) {
284
343
  const validationErrors = [];
285
- validationErrors.push(...validateIdSurface(entry.id));
344
+ validationErrors.push(...validateInstructionIdSurface(entry.id));
286
345
  for (const key of Object.keys(entry)) {
287
346
  if (!ALLOWED_INPUT_KEYS.has(key)) {
288
347
  validationErrors.push(`/: unexpected property "${key}"`);
@@ -292,6 +351,7 @@ function validateInstructionInputSurface(entry) {
292
351
  validationErrors.push(`/${key}: null is not allowed`);
293
352
  }
294
353
  validationErrors.push(...validateTypedInputShape(entry));
354
+ validationErrors.push(...validateInstructionInputEnumMembership(entry));
295
355
  return {
296
356
  record: entry,
297
357
  validationErrors: dedupe(validationErrors),
@@ -311,7 +371,7 @@ function validateInstructionRecord(entry) {
311
371
  if (record.classification !== undefined && !['public', 'internal', 'restricted'].includes(record.classification)) {
312
372
  validationErrors.push(`/classification: invalid value "${String(record.classification)}"`);
313
373
  }
314
- if (record.contentType !== undefined && !['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'].includes(record.contentType)) {
374
+ if (record.contentType !== undefined && !['instruction', 'template', 'workflow', 'reference', 'example', 'agent'].includes(record.contentType)) {
315
375
  validationErrors.push(`/contentType: invalid value "${String(record.contentType)}"`);
316
376
  }
317
377
  if (!validateInstructionSchema(record)) {
@@ -162,7 +162,7 @@ For full configuration options: see \`docs/mcp_configuration.md\` and \`docs/con
162
162
  categories: ['bootstrap', 'mcp-activation', 'quick-start', 'documentation'],
163
163
  owner: 'system',
164
164
  version: 3,
165
- schemaVersion: '4',
165
+ schemaVersion: '5',
166
166
  semanticSummary: 'Index Server quick start: search-first workflow, knowledge contribution, copilot instructions setup, and MCP client configuration for AI agents'
167
167
  }
168
168
  },
@@ -179,7 +179,7 @@ For full configuration options: see \`docs/mcp_configuration.md\` and \`docs/con
179
179
  categories: ['bootstrap', 'lifecycle'],
180
180
  owner: 'system',
181
181
  version: 1,
182
- schemaVersion: '4',
182
+ schemaVersion: '5',
183
183
  semanticSummary: 'Lifecycle and promotion guardrails after bootstrap confirmation',
184
184
  reviewIntervalDays: 120
185
185
  }
@@ -94,7 +94,7 @@ const INPUT_SCHEMAS = {
94
94
  keywords: { type: 'array', items: { type: 'string' }, description: 'Explicit keyword array for search action when the caller wants direct token control.' },
95
95
  ids: { type: 'array', items: { type: 'string' }, description: 'Array of instruction IDs for remove or export actions.' },
96
96
  category: { type: 'string', description: 'Filter by category for list action.' },
97
- contentType: { type: 'string', enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'], description: 'Filter by content type for list, search, or query actions, or specify the entry content type for add action.' },
97
+ contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session'], description: 'Filter by content type for list, search, or query actions, or specify the entry content type for add action. Legacy "chat-session" write inputs are migrated to "workflow".' },
98
98
  text: { type: 'string', description: 'Full-text search within query action.' },
99
99
  includeCategories: { type: 'boolean', description: 'Search categories in addition to id/title/semanticSummary/body for search action.' },
100
100
  caseSensitive: { type: 'boolean', description: 'Enable case-sensitive matching for search action.' },
@@ -148,7 +148,7 @@ const INPUT_SCHEMAS = {
148
148
  'index_import': { type: 'object', additionalProperties: false, properties: {
149
149
  entries: { oneOf: [
150
150
  { type: 'array', minItems: 1, items: { type: 'object', required: ['id', 'title', 'body', 'priority', 'audience', 'requirement'], additionalProperties: false, properties: {
151
- id: { type: 'string' }, title: { type: 'string' }, body: { type: 'string' }, rationale: { type: 'string' }, priority: { type: 'number' }, audience: { type: 'string' }, requirement: { type: 'string' }, categories: { type: 'array', items: { type: 'string' } }, version: { type: 'string' }, owner: { type: 'string' }, status: { type: 'string', enum: ['approved', 'draft', 'review', 'deprecated'] }, priorityTier: { type: 'string', enum: ['P1', 'P2', 'P3', 'P4'] }, classification: { type: 'string', enum: ['public', 'internal', 'restricted'] }, lastReviewedAt: { type: 'string' }, nextReviewDue: { type: 'string' }, semanticSummary: { type: 'string' }, changeLog: { type: 'array', items: { type: 'object', additionalProperties: true } }, contentType: { type: 'string', enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'] }, extensions: extensionsInputSchema, mode: { type: 'string' }
151
+ id: { type: 'string' }, title: { type: 'string' }, body: { type: 'string' }, rationale: { type: 'string' }, priority: { type: 'number' }, audience: { type: 'string' }, requirement: { type: 'string' }, categories: { type: 'array', items: { type: 'string' } }, version: { type: 'string' }, owner: { type: 'string' }, status: { type: 'string', enum: ['approved', 'draft', 'review', 'deprecated'] }, priorityTier: { type: 'string', enum: ['P1', 'P2', 'P3', 'P4'] }, classification: { type: 'string', enum: ['public', 'internal', 'restricted'] }, lastReviewedAt: { type: 'string' }, nextReviewDue: { type: 'string' }, semanticSummary: { type: 'string' }, changeLog: { type: 'array', items: { type: 'object', additionalProperties: true } }, contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session'] }, extensions: extensionsInputSchema, mode: { type: 'string' }
152
152
  } } },
153
153
  { type: 'string', description: 'Stringified JSON array of instruction entries, or a file path to a JSON array of instruction entries' }
154
154
  ] },
@@ -157,7 +157,7 @@ const INPUT_SCHEMAS = {
157
157
  } },
158
158
  'index_add': { type: 'object', additionalProperties: false, required: ['entry'], properties: {
159
159
  entry: { type: 'object', required: ['id', 'body'], additionalProperties: false, properties: {
160
- id: { type: 'string', maxLength: 120 }, title: { type: 'string' }, body: { type: 'string' }, rationale: { type: 'string' }, priority: { type: 'number' }, audience: { type: 'string' }, requirement: { type: 'string' }, categories: { type: 'array', items: { type: 'string' } }, deprecatedBy: { type: 'string' }, riskScore: { type: 'number' }, version: { type: 'string' }, owner: { type: 'string' }, status: { type: 'string', enum: ['approved', 'draft', 'review', 'deprecated'] }, priorityTier: { type: 'string', enum: ['P1', 'P2', 'P3', 'P4'] }, classification: { type: 'string', enum: ['public', 'internal', 'restricted'] }, lastReviewedAt: { type: 'string' }, nextReviewDue: { type: 'string' }, semanticSummary: { type: 'string' }, changeLog: { type: 'array', items: { type: 'object', additionalProperties: true } }, contentType: { type: 'string', enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'] }, extensions: extensionsInputSchema
160
+ id: { type: 'string', minLength: 1, maxLength: 120, pattern: '^[a-z0-9](?:[a-z0-9-_]{0,118}[a-z0-9])?$' }, title: { type: 'string' }, body: { type: 'string' }, rationale: { type: 'string' }, priority: { type: 'number', minimum: 1, maximum: 100 }, audience: { type: 'string', enum: ['individual', 'group', 'all'] }, requirement: { type: 'string', enum: ['mandatory', 'critical', 'recommended', 'optional', 'deprecated'] }, categories: { type: 'array', items: { type: 'string', pattern: '^[a-z0-9][a-z0-9-_]{0,48}$' } }, deprecatedBy: { type: 'string' }, riskScore: { type: 'number' }, version: { type: 'string' }, owner: { type: 'string' }, status: { type: 'string', enum: ['approved', 'draft', 'review', 'deprecated'] }, priorityTier: { type: 'string', enum: ['P1', 'P2', 'P3', 'P4'] }, classification: { type: 'string', enum: ['public', 'internal', 'restricted'] }, lastReviewedAt: { type: 'string' }, nextReviewDue: { type: 'string' }, semanticSummary: { type: 'string' }, changeLog: { type: 'array', items: { type: 'object', additionalProperties: true } }, contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session'] }, extensions: extensionsInputSchema
161
161
  } },
162
162
  overwrite: { type: 'boolean' },
163
163
  lax: { type: 'boolean' }
@@ -221,7 +221,7 @@ const INPUT_SCHEMAS = {
221
221
  limit: { type: 'number', minimum: 1, maximum: 100, default: 50, description: 'Maximum number of instruction IDs to return' },
222
222
  includeCategories: { type: 'boolean', default: false, description: 'Include categories in search scope' },
223
223
  caseSensitive: { type: 'boolean', default: false, description: 'Perform case-sensitive matching' },
224
- contentType: { type: 'string', enum: ['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'], description: 'Filter results by content type (optional)' }
224
+ contentType: { type: 'string', enum: ['instruction', 'template', 'workflow', 'reference', 'example', 'agent'], description: 'Filter results by content type (optional)' }
225
225
  } },
226
226
  // promote_from_repo tool
227
227
  'promote_from_repo': { type: 'object', additionalProperties: false, required: ['repoPath'], properties: {
@@ -321,15 +321,16 @@ INPUT_SCHEMAS['trace_dump'] = { type: 'object', additionalProperties: false, pro
321
321
  file: { type: 'string', description: 'Optional path to write the trace buffer JSON file' }
322
322
  } };
323
323
  // Stable & mutation classification lists (mirrors usage in toolHandlers; exported to remove duplication there).
324
- exports.STABLE = new Set(['health_check', 'graph_export', 'index_dispatch', 'index_search', 'index_governanceHash', 'prompt_review', 'integrity_verify', 'usage_track', 'usage_hotset', 'metrics_snapshot', 'gates_evaluate', 'meta_tools', 'help_overview', 'index_schema', 'manifest_status', 'index_diagnostics', 'meta_activation_guide', 'meta_check_activation', 'bootstrap', 'bootstrap_status', 'feature_status', 'index_health', 'index_inspect', 'index_debug', 'integrity_manifest', 'messaging_read', 'messaging_list_channels', 'messaging_stats', 'messaging_get', 'messaging_thread', 'trace_dump']);
325
- exports.MUTATION = new Set(['index_add', 'index_import', 'index_repair', 'index_reload', 'index_remove', 'index_groom', 'index_enrich', 'index_governanceUpdate', 'index_normalize', 'usage_flush', 'feedback_submit', 'manifest_refresh', 'manifest_repair', 'promote_from_repo', 'bootstrap_request', 'bootstrap_confirmFinalize', 'messaging_send', 'messaging_ack', 'messaging_update', 'messaging_purge', 'messaging_reply', 'diagnostics_block', 'diagnostics_microtaskFlood', 'diagnostics_memoryPressure']);
324
+ exports.STABLE = new Set(['health_check', 'feedback_submit', 'graph_export', 'index_dispatch', 'index_search', 'index_governanceHash', 'prompt_review', 'integrity_verify', 'usage_track', 'usage_hotset', 'metrics_snapshot', 'gates_evaluate', 'meta_tools', 'help_overview', 'index_schema', 'manifest_status', 'index_diagnostics', 'meta_activation_guide', 'meta_check_activation', 'bootstrap', 'bootstrap_status', 'feature_status', 'index_health', 'index_inspect', 'index_debug', 'integrity_manifest', 'messaging_read', 'messaging_list_channels', 'messaging_stats', 'messaging_get', 'messaging_thread', 'trace_dump']);
325
+ exports.MUTATION = new Set(['index_add', 'index_import', 'index_repair', 'index_reload', 'index_remove', 'index_groom', 'index_enrich', 'index_governanceUpdate', 'index_normalize', 'usage_flush', 'manifest_refresh', 'manifest_repair', 'promote_from_repo', 'bootstrap_request', 'bootstrap_confirmFinalize', 'messaging_send', 'messaging_ack', 'messaging_update', 'messaging_purge', 'messaging_reply', 'diagnostics_block', 'diagnostics_microtaskFlood', 'diagnostics_memoryPressure']);
326
326
  // Tool tier classification (002-tool-consolidation spec)
327
327
  // core: always visible, essential daily use
328
328
  // extended: opt-in via INDEX_SERVER_FLAG_TOOLS_EXTENDED=1 or flags.json tools_extended:true
329
329
  // admin: opt-in via INDEX_SERVER_FLAG_TOOLS_ADMIN=1, rarely needed ops/debug tools
330
330
  const TOOL_TIERS = {
331
- // Core (7 → 6 after feedback_dispatch removal)
331
+ // Core (7 after feedback_dispatch removal; feedback_submit remains always visible for agent reporting)
332
332
  'health_check': 'core',
333
+ 'feedback_submit': 'core',
333
334
  'index_dispatch': 'core',
334
335
  'index_search': 'core',
335
336
  'prompt_review': 'core',
@@ -351,7 +352,6 @@ const TOOL_TIERS = {
351
352
  'promote_from_repo': 'extended',
352
353
  'index_schema': 'extended',
353
354
  // Admin (everything else)
354
- 'feedback_submit': 'admin',
355
355
  'meta_tools': 'admin',
356
356
  'meta_activation_guide': 'admin',
357
357
  'meta_check_activation': 'admin',
@@ -35,7 +35,7 @@ const zDispatch = zod_1.z.object({
35
35
  keywords: zod_1.z.array(zod_1.z.string()).optional(),
36
36
  ids: zod_1.z.array(zod_1.z.string()).optional(),
37
37
  category: zod_1.z.string().optional(),
38
- contentType: zod_1.z.enum(['instruction', 'template', 'chat-session', 'reference', 'example', 'agent']).optional(),
38
+ contentType: zod_1.z.enum(['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session']).optional(),
39
39
  text: zod_1.z.string().optional(),
40
40
  includeCategories: zod_1.z.boolean().optional(),
41
41
  caseSensitive: zod_1.z.boolean().optional(),
@@ -98,7 +98,7 @@ function buildZIndexEntry() {
98
98
  nextReviewDue: zod_1.z.string().optional(),
99
99
  semanticSummary: zod_1.z.string().optional(),
100
100
  changeLog: zod_1.z.array(zod_1.z.object({}).passthrough()).optional(),
101
- contentType: zod_1.z.enum(['instruction', 'template', 'chat-session', 'reference', 'example', 'agent']).optional(),
101
+ contentType: zod_1.z.enum(['instruction', 'template', 'workflow', 'reference', 'example', 'agent', 'chat-session']).optional(),
102
102
  extensions: zod_1.z.record(zod_1.z.string(), zExtensionValue).optional()
103
103
  }).strict();
104
104
  }
@@ -166,7 +166,7 @@ const zSearch = zod_1.z.object({
166
166
  limit: zod_1.z.number().int().min(1).max(100).default(50).optional(),
167
167
  includeCategories: zod_1.z.boolean().default(false).optional(),
168
168
  caseSensitive: zod_1.z.boolean().default(false).optional(),
169
- contentType: zod_1.z.enum(['instruction', 'template', 'chat-session', 'reference', 'example', 'agent']).optional()
169
+ contentType: zod_1.z.enum(['instruction', 'template', 'workflow', 'reference', 'example', 'agent']).optional()
170
170
  }).strict();
171
171
  // ── Diagnostics ──────────────────────────────────────────────────────────────
172
172
  const zDiagnostics = zod_1.z.object({
@@ -1,4 +1,4 @@
1
- export declare const SCHEMA_VERSION = "4";
1
+ export declare const SCHEMA_VERSION = "5";
2
2
  export interface MigrationResult {
3
3
  changed: boolean;
4
4
  notes?: string[];