@jagilber-org/index-server 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/CONTRIBUTING.md +3 -3
- package/dist/dashboard/client/admin.html +58 -28
- package/dist/dashboard/client/css/admin.css +54 -0
- package/dist/dashboard/client/js/admin.config.js +3 -6
- package/dist/dashboard/client/js/admin.embeddings.js +63 -4
- package/dist/dashboard/client/js/admin.events.js +256 -0
- package/dist/dashboard/client/js/admin.feedback.js +1 -1
- package/dist/dashboard/client/js/admin.instructions.js +1 -1
- package/dist/dashboard/client/js/admin.maintenance.js +75 -32
- package/dist/dashboard/client/js/admin.sessions.js +1 -1
- package/dist/dashboard/security/SecurityMonitor.js +2 -2
- package/dist/dashboard/server/AdminPanel.js +83 -6
- package/dist/dashboard/server/AdminPanelConfig.d.ts +11 -0
- package/dist/dashboard/server/AdminPanelConfig.js +47 -17
- package/dist/dashboard/server/AdminPanelState.js +5 -1
- package/dist/dashboard/server/ApiRoutes.js +2 -1
- package/dist/dashboard/server/DashboardServer.js +13 -0
- package/dist/dashboard/server/MetricsCollector.js +3 -2
- package/dist/dashboard/server/WebSocketManager.js +2 -2
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
- package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +1 -1
- package/dist/dashboard/server/routes/admin.routes.js +143 -17
- package/dist/dashboard/server/routes/api.usage.routes.js +5 -1
- package/dist/dashboard/server/routes/embeddings.routes.js +91 -1
- package/dist/dashboard/server/routes/instructions.routes.js +142 -12
- package/dist/dashboard/server/routes/scripts.routes.js +1 -1
- package/dist/dashboard/server/routes/sqlite.routes.js +74 -0
- package/dist/models/instruction.d.ts +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/server/sdkServer.js +12 -4
- package/dist/services/auditLog.d.ts +1 -1
- package/dist/services/auditLog.js +1 -1
- package/dist/services/embeddingService.d.ts +2 -0
- package/dist/services/embeddingService.js +16 -4
- package/dist/services/embeddingTrigger.d.ts +33 -0
- package/dist/services/embeddingTrigger.js +86 -0
- package/dist/services/eventBuffer.d.ts +45 -0
- package/dist/services/eventBuffer.js +110 -0
- package/dist/services/feedbackStorage.js +1 -1
- package/dist/services/handlers/instructions.add.js +36 -3
- package/dist/services/handlers/instructions.import.js +71 -13
- package/dist/services/handlers.dashboardConfig.js +81 -0
- package/dist/services/handlers.feedback.js +1 -1
- package/dist/services/handlers.instructionSchema.js +4 -4
- package/dist/services/handlers.search.js +3 -3
- package/dist/services/indexContext.d.ts +18 -0
- package/dist/services/indexContext.js +133 -24
- package/dist/services/instructionRecordValidation.d.ts +3 -0
- package/dist/services/instructionRecordValidation.js +64 -4
- package/dist/services/logger.js +9 -0
- package/dist/services/manifestManager.js +11 -1
- package/dist/services/seedBootstrap.js +7 -3
- package/dist/services/storage/factory.d.ts +2 -0
- package/dist/services/storage/factory.js +12 -1
- package/dist/services/toolRegistry.js +8 -8
- package/dist/services/toolRegistry.zod.js +3 -3
- package/dist/services/tracing.js +3 -1
- package/dist/versioning/schemaVersion.d.ts +1 -1
- package/dist/versioning/schemaVersion.js +47 -2
- package/package.json +54 -40
- package/schemas/index-server.code-schema.json +1 -1
- package/schemas/instruction.schema.json +3 -3
- package/schemas/json-schema/instruction-content-type.schema.json +1 -1
- package/schemas/json-schema/instruction-instruction-entry.schema.json +1 -1
- package/scripts/README.md +48 -0
- package/scripts/{generate-certs.mjs → build/generate-certs.mjs} +1 -1
- package/scripts/{setup-wizard.mjs → build/setup-wizard.mjs} +1 -1
- package/scripts/{setup-hooks.cjs → hooks/setup-hooks.cjs} +3 -3
- package/server.json +3 -3
- /package/scripts/{copy-dashboard-assets.mjs → build/copy-dashboard-assets.mjs} +0 -0
|
@@ -3,6 +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._internal = void 0;
|
|
7
|
+
exports._resetIndexContextProcessLatches = _resetIndexContextProcessLatches;
|
|
8
|
+
exports._resetIndexContextStateForTests = _resetIndexContextStateForTests;
|
|
6
9
|
exports.getInvariantRepairSummary = getInvariantRepairSummary;
|
|
7
10
|
exports.clearUsageRateLimit = clearUsageRateLimit;
|
|
8
11
|
exports.loadUsageSnapshot = loadUsageSnapshot;
|
|
@@ -95,6 +98,33 @@ function trackInvariantRepair(id, field, source) {
|
|
|
95
98
|
if (invariantRepairLog.length > MAX_REPAIR_LOG)
|
|
96
99
|
invariantRepairLog.shift();
|
|
97
100
|
}
|
|
101
|
+
// ── Process-scoped latches for noise + work suppression ──────────
|
|
102
|
+
// Symptom that motivated these latches (observed live on dev port 8687,
|
|
103
|
+
// 2026-05-01): every dashboard request triggered ensureLoaded() →
|
|
104
|
+
// migrateJsonToSqlite() because jsonFiles.length > sqliteRowCount was
|
|
105
|
+
// permanently true (a few JSON files failed loader validation), and every
|
|
106
|
+
// /api/admin/stats request emitted hundreds of stack-traced WARN entries
|
|
107
|
+
// from restoreFirstSeenInvariant for entries whose firstSeenTs was
|
|
108
|
+
// genuinely unrecoverable. Both of those are infinite-cost loops once the
|
|
109
|
+
// process is up. We dedupe both per-process here.
|
|
110
|
+
const autoMigrationAttempted = new Set();
|
|
111
|
+
const firstSeenExhaustedReported = new Set();
|
|
112
|
+
/**
|
|
113
|
+
* Test-only hook — reset process-scoped latches between vitest specs.
|
|
114
|
+
* @internal Not part of the public API.
|
|
115
|
+
*/
|
|
116
|
+
function _resetIndexContextProcessLatches() {
|
|
117
|
+
autoMigrationAttempted.clear();
|
|
118
|
+
firstSeenExhaustedReported.clear();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Test-only hook — reset the module-scoped index state cache.
|
|
122
|
+
* @internal Not part of the public API.
|
|
123
|
+
*/
|
|
124
|
+
function _resetIndexContextStateForTests() {
|
|
125
|
+
state = null;
|
|
126
|
+
dirty = false;
|
|
127
|
+
}
|
|
98
128
|
/** Returns a summary of invariant repairs for health check visibility. */
|
|
99
129
|
function getInvariantRepairSummary() {
|
|
100
130
|
return { totalRepairs: invariantRepairLog.length, recentRepairs: invariantRepairLog.slice(-20) };
|
|
@@ -110,7 +140,7 @@ function restoreFirstSeenInvariant(e) {
|
|
|
110
140
|
e.firstSeenTs = auth;
|
|
111
141
|
(0, features_1.incrementCounter)('usage:firstSeenAuthorityRepair');
|
|
112
142
|
trackInvariantRepair(e.id, 'firstSeenTs', 'authority');
|
|
113
|
-
(0, logger_js_1.
|
|
143
|
+
(0, logger_js_1.logDebug)(`[invariant-repair] firstSeenTs restored from authority for ${e.id}`);
|
|
114
144
|
return;
|
|
115
145
|
}
|
|
116
146
|
const ep = ephemeralFirstSeen[e.id];
|
|
@@ -118,7 +148,7 @@ function restoreFirstSeenInvariant(e) {
|
|
|
118
148
|
e.firstSeenTs = ep;
|
|
119
149
|
(0, features_1.incrementCounter)('usage:firstSeenInvariantRepair');
|
|
120
150
|
trackInvariantRepair(e.id, 'firstSeenTs', 'ephemeral');
|
|
121
|
-
(0, logger_js_1.
|
|
151
|
+
(0, logger_js_1.logDebug)(`[invariant-repair] firstSeenTs restored from ephemeral cache for ${e.id}`);
|
|
122
152
|
return;
|
|
123
153
|
}
|
|
124
154
|
const snap = lastGoodUsageSnapshot[e.id];
|
|
@@ -126,15 +156,40 @@ function restoreFirstSeenInvariant(e) {
|
|
|
126
156
|
e.firstSeenTs = snap.firstSeenTs;
|
|
127
157
|
(0, features_1.incrementCounter)('usage:firstSeenInvariantRepair');
|
|
128
158
|
trackInvariantRepair(e.id, 'firstSeenTs', 'snapshot');
|
|
129
|
-
(0, logger_js_1.
|
|
159
|
+
(0, logger_js_1.logDebug)(`[invariant-repair] firstSeenTs restored from snapshot for ${e.id}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Final fallback: createdAt. By definition firstSeenTs ≤ createdAt is impossible
|
|
163
|
+
// (the index can never have observed an entry before it was created). For
|
|
164
|
+
// freshly-imported / freshly-added entries with no usage history yet, this is
|
|
165
|
+
// the correct answer; for legacy on-disk entries written before write-path
|
|
166
|
+
// populated firstSeenTs, it heals them silently. Repaired silently (no WARN)
|
|
167
|
+
// because this is the documented authoritative semantic, not a defect.
|
|
168
|
+
if (e.createdAt) {
|
|
169
|
+
e.firstSeenTs = e.createdAt;
|
|
170
|
+
firstSeenAuthority[e.id] = e.createdAt;
|
|
171
|
+
(0, features_1.incrementCounter)('usage:firstSeenCreatedAtFallback');
|
|
172
|
+
trackInvariantRepair(e.id, 'firstSeenTs', 'createdAt');
|
|
173
|
+
return;
|
|
130
174
|
}
|
|
131
|
-
// If still missing after all repair sources, track an exhausted repair attempt (extremely rare diagnostic)
|
|
175
|
+
// If still missing after all repair sources, track an exhausted repair attempt (extremely rare diagnostic).
|
|
176
|
+
// Dedup the WARN per-id-per-process: a permanently unrecoverable id otherwise spams hundreds of
|
|
177
|
+
// stack-traced WARNs per dashboard poll (RCA 2026-05-01, dev port 8687). The counter and audit
|
|
178
|
+
// trail still increment on every call so health metrics remain accurate.
|
|
132
179
|
if (!e.firstSeenTs) {
|
|
133
180
|
(0, features_1.incrementCounter)('usage:firstSeenRepairExhausted');
|
|
134
181
|
trackInvariantRepair(e.id, 'firstSeenTs', 'exhausted');
|
|
135
|
-
(
|
|
182
|
+
if (!firstSeenExhaustedReported.has(e.id)) {
|
|
183
|
+
firstSeenExhaustedReported.add(e.id);
|
|
184
|
+
(0, logger_js_1.logWarn)(`[invariant-repair] firstSeenTs repair exhausted — no source found for ${e.id}`);
|
|
185
|
+
}
|
|
136
186
|
}
|
|
137
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Internal handles for unit tests only. Not part of the public API.
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
exports._internal = { restoreFirstSeenInvariant };
|
|
138
193
|
// Usage invariant repair (mirrors firstSeen invariant strategy). Extremely rare reload races in CI produced
|
|
139
194
|
// states where a freshly re-materialized InstructionEntry temporarily lacked its prior usageCount (observed
|
|
140
195
|
// by usageTracking.spec snapshot reads) even though authority maps retained the correct monotonic value.
|
|
@@ -421,14 +476,30 @@ function touchIndexVersion() {
|
|
|
421
476
|
}
|
|
422
477
|
catch { /* ignore */ }
|
|
423
478
|
}
|
|
424
|
-
function readVersionMTime() {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
479
|
+
function readVersionMTime() {
|
|
480
|
+
try {
|
|
481
|
+
const vf = getVersionFile();
|
|
482
|
+
if (fs_1.default.existsSync(vf)) {
|
|
483
|
+
const st = fs_1.default.statSync(vf);
|
|
484
|
+
return st.mtimeMs || 0;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch { /* ignore */ }
|
|
488
|
+
// Fallback: when no .index-version file exists, use the instructions
|
|
489
|
+
// directory's own mtime so the ensureLoaded() cache short-circuit can still
|
|
490
|
+
// recognize an unchanged source. RCA 2026-05-01 (live dev port 8787 vs an
|
|
491
|
+
// operator-explored repo with no version file): readVersionMTime() returned
|
|
492
|
+
// 0, the falsy short-circuit `if (currentVersionMTime && ...)` always failed,
|
|
493
|
+
// every dashboard poll triggered a full disk reload + simple-reload trace,
|
|
494
|
+
// saturating the event loop and breaking dashboard imports.
|
|
495
|
+
try {
|
|
496
|
+
const baseDir = getInstructionsDir();
|
|
497
|
+
const st = fs_1.default.statSync(baseDir);
|
|
428
498
|
return st.mtimeMs || 0;
|
|
429
499
|
}
|
|
500
|
+
catch { /* ignore */ }
|
|
501
|
+
return 0;
|
|
430
502
|
}
|
|
431
|
-
catch { /* ignore */ } return 0; }
|
|
432
503
|
function readVersionToken() { try {
|
|
433
504
|
const vf = getVersionFile();
|
|
434
505
|
if (fs_1.default.existsSync(vf)) {
|
|
@@ -503,20 +574,30 @@ function ensureLoaded() {
|
|
|
503
574
|
const backend = (0, runtimeConfig_1.getRuntimeConfig)().storage?.backend ?? 'json';
|
|
504
575
|
const store = backend === 'sqlite' ? getStoreForDir(baseDir) : null;
|
|
505
576
|
let result = store ? store.load() : new indexLoader_1.IndexLoader(baseDir).load();
|
|
506
|
-
// Auto-migrate JSON → SQLite when JSON files on disk outnumber SQLite rows
|
|
577
|
+
// Auto-migrate JSON → SQLite when JSON files on disk outnumber SQLite rows.
|
|
578
|
+
// Per-process latch (RCA 2026-05-01): without this, mismatched counts caused by
|
|
579
|
+
// a few unparseable JSON files (jsonFiles.length permanently > sqlite rows) made
|
|
580
|
+
// ensureLoaded() re-invoke migrateJsonToSqlite() on every reload tick, causing
|
|
581
|
+
// INSERT-OR-REPLACE storms and unbounded WAL growth (~1.21 GB observed in dev
|
|
582
|
+
// before the fix). One attempt per (baseDir, dbPath) per process is enough; if
|
|
583
|
+
// an operator truly needs a re-migrate, they restart the server.
|
|
507
584
|
if (store && (0, runtimeConfig_1.getRuntimeConfig)().storage?.sqliteMigrateOnStart) {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
585
|
+
const dbPath = (0, runtimeConfig_1.getRuntimeConfig)().storage?.sqlitePath ?? path_1.default.join(process.cwd(), 'data', 'index.db');
|
|
586
|
+
const latchKey = `${baseDir}|${dbPath}`;
|
|
587
|
+
if (!autoMigrationAttempted.has(latchKey)) {
|
|
588
|
+
autoMigrationAttempted.add(latchKey);
|
|
589
|
+
const jsonFiles = fs_1.default.existsSync(baseDir) ? fs_1.default.readdirSync(baseDir).filter(f => f.endsWith('.json') && !f.startsWith('_')) : [];
|
|
590
|
+
if (jsonFiles.length > result.entries.length) {
|
|
591
|
+
try {
|
|
592
|
+
const mr = (0, migrationEngine_1.migrateJsonToSqlite)(baseDir, dbPath);
|
|
593
|
+
if (mr.migrated > 0) {
|
|
594
|
+
(0, logger_js_1.logInfo)(`[storage] Auto-migrated ${mr.migrated} instruction(s) from JSON → SQLite`);
|
|
595
|
+
result = store.load();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
(0, logger_js_1.logWarn)('[storage] Auto-migration failed:', err);
|
|
516
600
|
}
|
|
517
|
-
}
|
|
518
|
-
catch (err) {
|
|
519
|
-
(0, logger_js_1.logWarn)('[storage] Auto-migration failed:', err);
|
|
520
601
|
}
|
|
521
602
|
}
|
|
522
603
|
}
|
|
@@ -525,7 +606,17 @@ function ensureLoaded() {
|
|
|
525
606
|
// Deduplicate list using byId so two on-disk files with the same id field never produce duplicate
|
|
526
607
|
// search results. byId already uses last-write-wins semantics; list must be consistent with it.
|
|
527
608
|
const deduplicatedList = Array.from(byId.values());
|
|
528
|
-
|
|
609
|
+
// RCA 2026-05-01 (live dev port 8787): IndexLoader.load() writes _manifest.json
|
|
610
|
+
// and _skipped.json into baseDir as a side-effect, which bumps the directory's
|
|
611
|
+
// mtime. If we cached the pre-load mtime here, the very next ensureLoaded()
|
|
612
|
+
// call would observe a newer mtime, miss the cache, and reload again — an
|
|
613
|
+
// unbounded loop that emitted [trace:ensureLoaded:simple-reload] hundreds of
|
|
614
|
+
// times per second and saturated the event loop (dashboard imports failed).
|
|
615
|
+
// Re-read the mtime AFTER load so the cached value reflects the post-write
|
|
616
|
+
// state; subsequent calls without source changes will then short-circuit.
|
|
617
|
+
const postLoadVersionMTime = readVersionMTime();
|
|
618
|
+
const postLoadVersionToken = readVersionToken();
|
|
619
|
+
state = { loadedAt: new Date().toISOString(), hash: result.hash, byId, list: deduplicatedList, fileCount: deduplicatedList.length, versionMTime: postLoadVersionMTime || currentVersionMTime, versionToken: postLoadVersionToken || currentVersionToken, loadErrors: result.errors, loadDebug: result.debug, loadSummary: result.summary };
|
|
529
620
|
dirty = false;
|
|
530
621
|
// Overlay usage snapshot (simplified; no spin/repair loops here—existing invariant repairs still occur in getIndexState)
|
|
531
622
|
try {
|
|
@@ -573,7 +664,11 @@ async function ensureLoadedAsync() {
|
|
|
573
664
|
const byId = new Map();
|
|
574
665
|
result.entries.forEach(e => byId.set(e.id, e));
|
|
575
666
|
const deduplicatedList = Array.from(byId.values());
|
|
576
|
-
|
|
667
|
+
// See RCA comment in ensureLoaded() above: re-read mtime AFTER load to absorb
|
|
668
|
+
// _manifest.json / _skipped.json side-effect writes.
|
|
669
|
+
const postLoadVersionMTime = readVersionMTime();
|
|
670
|
+
const postLoadVersionToken = readVersionToken();
|
|
671
|
+
state = { loadedAt: new Date().toISOString(), hash: result.hash, byId, list: deduplicatedList, fileCount: deduplicatedList.length, versionMTime: postLoadVersionMTime || currentVersionMTime, versionToken: postLoadVersionToken || currentVersionToken, loadErrors: result.errors, loadDebug: result.debug, loadSummary: result.summary };
|
|
577
672
|
dirty = false;
|
|
578
673
|
try {
|
|
579
674
|
const snap = loadUsageSnapshot();
|
|
@@ -826,6 +921,15 @@ function isDuplicateInstructionWriteError(error) {
|
|
|
826
921
|
}
|
|
827
922
|
function writeEntry(entry, opts) {
|
|
828
923
|
const file = path_1.default.join(getInstructionsDir(), `${entry.id}.json`);
|
|
924
|
+
// Establish firstSeenTs at the write boundary if the caller omitted it.
|
|
925
|
+
// Semantically firstSeenTs ≤ createdAt always — for fresh entries this is
|
|
926
|
+
// the correct value, and persisting it on disk avoids spurious
|
|
927
|
+
// [invariant-repair] WARN spam on every subsequent getIndexState() poll
|
|
928
|
+
// (RCA 2026-05-01 dev port 8687, third loop in chain after PR #285/#286).
|
|
929
|
+
if (!entry.firstSeenTs) {
|
|
930
|
+
entry.firstSeenTs = entry.createdAt || new Date().toISOString();
|
|
931
|
+
firstSeenAuthority[entry.id] = entry.firstSeenTs;
|
|
932
|
+
}
|
|
829
933
|
const classifier = new classificationService_1.ClassificationService();
|
|
830
934
|
let record = classifier.normalize(entry);
|
|
831
935
|
if (record.owner === 'unowned') {
|
|
@@ -880,6 +984,11 @@ function writeEntry(entry, opts) {
|
|
|
880
984
|
}
|
|
881
985
|
async function writeEntryAsync(entry, opts) {
|
|
882
986
|
const file = path_1.default.join(getInstructionsDir(), `${entry.id}.json`);
|
|
987
|
+
// See writeEntry: establish firstSeenTs at the write boundary if missing.
|
|
988
|
+
if (!entry.firstSeenTs) {
|
|
989
|
+
entry.firstSeenTs = entry.createdAt || new Date().toISOString();
|
|
990
|
+
firstSeenAuthority[entry.id] = entry.firstSeenTs;
|
|
991
|
+
}
|
|
883
992
|
const classifier = new classificationService_1.ClassificationService();
|
|
884
993
|
let record = classifier.normalize(entry);
|
|
885
994
|
if (record.owner === 'unowned') {
|
|
@@ -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
|
|
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(...
|
|
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', '
|
|
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)) {
|
package/dist/services/logger.js
CHANGED
|
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
13
13
|
const mcpLogBridge_1 = require("./mcpLogBridge");
|
|
14
|
+
const eventBuffer_1 = require("./eventBuffer");
|
|
14
15
|
/** Numeric priority for log level filtering (lower = more verbose). */
|
|
15
16
|
const LEVEL_PRIORITY = {
|
|
16
17
|
TRACE: 0,
|
|
@@ -231,6 +232,14 @@ function emit(rec) {
|
|
|
231
232
|
}
|
|
232
233
|
catch { /* ignore file write failures */ }
|
|
233
234
|
}
|
|
235
|
+
// Surface WARN/ERROR into the in-process events ring buffer so the dashboard
|
|
236
|
+
// Monitoring panel can display them without log-file tailing (OB-3, OB-5).
|
|
237
|
+
if (rec.level === 'WARN' || rec.level === 'ERROR') {
|
|
238
|
+
try {
|
|
239
|
+
(0, eventBuffer_1.recordEvent)(rec.level, rec.msg, rec.detail, rec.pid);
|
|
240
|
+
}
|
|
241
|
+
catch { /* never let buffer failure break logging */ }
|
|
242
|
+
}
|
|
234
243
|
}
|
|
235
244
|
/**
|
|
236
245
|
* Emit a structured NDJSON log record at the specified level.
|
|
@@ -20,7 +20,17 @@ const features_1 = require("./features");
|
|
|
20
20
|
const logger_1 = require("./logger");
|
|
21
21
|
const runtimeConfig_1 = require("../config/runtimeConfig");
|
|
22
22
|
const MANIFEST_RELATIVE = path_1.default.join('snapshots', 'index-manifest.json');
|
|
23
|
-
function getManifestPath() {
|
|
23
|
+
function getManifestPath() {
|
|
24
|
+
// Tests run in parallel forks (vitest pool=forks, maxWorkers=4) and each fork
|
|
25
|
+
// shares the same process.cwd(); without an override, multiple test files that
|
|
26
|
+
// enable manifest write race on the same on-disk file. Honor an explicit path
|
|
27
|
+
// override so test setup can isolate per-spec output. Production leaves this
|
|
28
|
+
// unset and continues to use the canonical snapshot path.
|
|
29
|
+
const override = process.env.INDEX_SERVER_MANIFEST_PATH;
|
|
30
|
+
if (override && override.trim())
|
|
31
|
+
return path_1.default.isAbsolute(override) ? override : path_1.default.join(process.cwd(), override);
|
|
32
|
+
return path_1.default.join(process.cwd(), MANIFEST_RELATIVE);
|
|
33
|
+
}
|
|
24
34
|
/**
|
|
25
35
|
* Load the manifest from disk.
|
|
26
36
|
* @returns The parsed {@link IndexManifest}, or `null` if the file is absent or unparseable
|
|
@@ -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: '
|
|
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: '
|
|
182
|
+
schemaVersion: '5',
|
|
183
183
|
semanticSummary: 'Lifecycle and promotion guardrails after bootstrap confirmation',
|
|
184
184
|
reviewIntervalDays: 120
|
|
185
185
|
}
|
|
@@ -223,7 +223,11 @@ function autoSeedBootstrap() {
|
|
|
223
223
|
// Directory empty OR missing seed triggers creation.
|
|
224
224
|
try {
|
|
225
225
|
const tmp = path_1.default.join(dir, `.${seed.file}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
226
|
-
|
|
226
|
+
// Inject timestamps at write time so loaders never trigger
|
|
227
|
+
// [invariant-repair] firstSeenTs WARN noise on subsequent reads.
|
|
228
|
+
const nowIso = new Date().toISOString();
|
|
229
|
+
const stamped = { createdAt: nowIso, firstSeenTs: nowIso, ...seed.json };
|
|
230
|
+
fs_1.default.writeFileSync(tmp, JSON.stringify(stamped, null, 2), { encoding: 'utf8' });
|
|
227
231
|
fs_1.default.renameSync(tmp, target);
|
|
228
232
|
summary.created.push(seed.file);
|
|
229
233
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { IInstructionStore, IEmbeddingStore } from './types.js';
|
|
8
8
|
export type StorageBackend = 'json' | 'sqlite';
|
|
9
|
+
/** Test-only: reset the warn-once latch. */
|
|
10
|
+
export declare function _resetSqliteExperimentalWarning(): void;
|
|
9
11
|
/**
|
|
10
12
|
* Check that the current Node.js version meets the minimum requirement.
|
|
11
13
|
* Throws a clear error if the version is too old.
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Default: JsonFileStore (json). Experimental: SqliteStore (sqlite).
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports._resetSqliteExperimentalWarning = _resetSqliteExperimentalWarning;
|
|
9
10
|
exports.checkNodeVersion = checkNodeVersion;
|
|
10
11
|
exports.createStore = createStore;
|
|
11
12
|
exports.createEmbeddingStore = createEmbeddingStore;
|
|
@@ -14,6 +15,13 @@ const jsonFileStore_js_1 = require("./jsonFileStore.js");
|
|
|
14
15
|
const jsonEmbeddingStore_js_1 = require("./jsonEmbeddingStore.js");
|
|
15
16
|
const sqliteStore_js_1 = require("./sqliteStore.js");
|
|
16
17
|
const logger_js_1 = require("../logger.js");
|
|
18
|
+
/** Process-scoped flag so the EXPERIMENTAL SQLite warning is emitted once,
|
|
19
|
+
* not per-store-creation (which floods the events ring on every request). */
|
|
20
|
+
let sqliteExperimentalWarned = false;
|
|
21
|
+
/** Test-only: reset the warn-once latch. */
|
|
22
|
+
function _resetSqliteExperimentalWarning() {
|
|
23
|
+
sqliteExperimentalWarned = false;
|
|
24
|
+
}
|
|
17
25
|
/**
|
|
18
26
|
* Check that the current Node.js version meets the minimum requirement.
|
|
19
27
|
* Throws a clear error if the version is too old.
|
|
@@ -46,7 +54,10 @@ function createStore(backend, dir, sqlitePath) {
|
|
|
46
54
|
switch (resolvedBackend) {
|
|
47
55
|
case 'sqlite': {
|
|
48
56
|
checkNodeVersion('22.5.0', 'SQLite storage backend (node:sqlite)');
|
|
49
|
-
(
|
|
57
|
+
if (!sqliteExperimentalWarned) {
|
|
58
|
+
sqliteExperimentalWarned = true;
|
|
59
|
+
(0, logger_js_1.logWarn)('[storage] ⚠️ EXPERIMENTAL: SQLite backend is enabled. This feature has limited testing and may have data-loss or compatibility issues. Not recommended for production use.');
|
|
60
|
+
}
|
|
50
61
|
const dbPath = sqlitePath ?? config.storage?.sqlitePath ?? 'data/index.db';
|
|
51
62
|
return new sqliteStore_js_1.SqliteStore(dbPath);
|
|
52
63
|
}
|
|
@@ -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', '
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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({
|
package/dist/services/tracing.js
CHANGED
|
@@ -219,8 +219,10 @@ function emitTrace(label, data, min = 1) {
|
|
|
219
219
|
if (caller)
|
|
220
220
|
rec.func = caller;
|
|
221
221
|
pushBuffer(rec, tracing.buffer);
|
|
222
|
+
// Trace records are diagnostic, not errors. Route through DEBUG so they do
|
|
223
|
+
// not pollute the WARN/ERROR events ring buffer surfaced to the dashboard.
|
|
222
224
|
try {
|
|
223
|
-
(0, logger_js_1.
|
|
225
|
+
(0, logger_js_1.logDebug)(label, JSON.stringify(rec));
|
|
224
226
|
}
|
|
225
227
|
catch { /* ignore */ }
|
|
226
228
|
if (!(tracing.persist || tracing.file))
|