@jagilber-org/index-server 1.22.1 → 1.26.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 (189) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/CODE_OF_CONDUCT.md +2 -0
  3. package/CONTRIBUTING.md +32 -2
  4. package/README.md +82 -19
  5. package/SECURITY.md +17 -5
  6. package/dist/config/dashboardConfig.d.ts +3 -0
  7. package/dist/config/dashboardConfig.js +3 -0
  8. package/dist/config/defaultValues.d.ts +1 -1
  9. package/dist/config/defaultValues.js +1 -1
  10. package/dist/config/featureConfig.d.ts +2 -0
  11. package/dist/config/featureConfig.js +6 -1
  12. package/dist/config/runtimeConfig.d.ts +1 -1
  13. package/dist/config/runtimeConfig.js +8 -9
  14. package/dist/dashboard/client/admin.html +170 -53
  15. package/dist/dashboard/client/css/admin.css +132 -0
  16. package/dist/dashboard/client/js/admin.auth.js +25 -11
  17. package/dist/dashboard/client/js/admin.config.js +1 -1
  18. package/dist/dashboard/client/js/admin.feedback.js +328 -0
  19. package/dist/dashboard/client/js/admin.graph.js +120 -18
  20. package/dist/dashboard/client/js/admin.instructions.js +27 -13
  21. package/dist/dashboard/client/js/admin.logs.js +1 -5
  22. package/dist/dashboard/client/js/admin.maintenance.js +53 -8
  23. package/dist/dashboard/client/js/admin.messaging.js +1 -4
  24. package/dist/dashboard/client/js/admin.overview.js +5 -1
  25. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  26. package/dist/dashboard/client/js/admin.utils.js +43 -1
  27. package/dist/dashboard/client/js/mermaid.min.js +813 -537
  28. package/dist/dashboard/export/DataExporter.js +2 -1
  29. package/dist/dashboard/server/AdminPanel.d.ts +3 -0
  30. package/dist/dashboard/server/AdminPanel.js +132 -35
  31. package/dist/dashboard/server/ApiRoutes.js +40 -9
  32. package/dist/dashboard/server/DashboardServer.js +1 -1
  33. package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
  34. package/dist/dashboard/server/FileMetricsStorage.js +52 -5
  35. package/dist/dashboard/server/HttpTransport.js +6 -0
  36. package/dist/dashboard/server/InstanceManager.js +7 -2
  37. package/dist/dashboard/server/KnowledgeStore.js +7 -2
  38. package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
  39. package/dist/dashboard/server/MetricsCollector.js +113 -17
  40. package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
  41. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  42. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
  43. package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
  44. package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
  45. package/dist/dashboard/server/routes/admin.routes.js +35 -27
  46. package/dist/dashboard/server/routes/alerts.routes.js +4 -3
  47. package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
  48. package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
  49. package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
  50. package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
  51. package/dist/dashboard/server/routes/graph.routes.js +10 -13
  52. package/dist/dashboard/server/routes/index.d.ts +1 -0
  53. package/dist/dashboard/server/routes/index.js +74 -39
  54. package/dist/dashboard/server/routes/instances.routes.js +2 -1
  55. package/dist/dashboard/server/routes/instructions.routes.js +46 -27
  56. package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
  57. package/dist/dashboard/server/routes/logs.routes.js +5 -4
  58. package/dist/dashboard/server/routes/messaging.routes.js +15 -14
  59. package/dist/dashboard/server/routes/metrics.routes.js +14 -13
  60. package/dist/dashboard/server/routes/scripts.routes.js +6 -3
  61. package/dist/dashboard/server/routes/status.routes.js +5 -4
  62. package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
  63. package/dist/dashboard/server/routes/usage.routes.js +2 -1
  64. package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
  65. package/dist/dashboard/server/utils/escapeHtml.js +11 -0
  66. package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
  67. package/dist/dashboard/server/utils/pathContainment.js +15 -0
  68. package/dist/dashboard/server/wsInit.js +2 -2
  69. package/dist/lib/mcpStdioLogging.d.ts +165 -0
  70. package/dist/lib/mcpStdioLogging.js +287 -0
  71. package/dist/schemas/index.d.ts +37 -2
  72. package/dist/schemas/index.js +27 -3
  73. package/dist/server/backgroundServicesStartup.d.ts +7 -1
  74. package/dist/server/backgroundServicesStartup.js +25 -8
  75. package/dist/server/certInit.d.ts +97 -0
  76. package/dist/server/certInit.js +359 -0
  77. package/dist/server/certInit.types.d.ts +92 -0
  78. package/dist/server/certInit.types.js +34 -0
  79. package/dist/server/handshake/fallbackFrames.d.ts +31 -0
  80. package/dist/server/handshake/fallbackFrames.js +38 -0
  81. package/dist/server/handshake/initializeDetector.d.ts +31 -0
  82. package/dist/server/handshake/initializeDetector.js +88 -0
  83. package/dist/server/handshake/protocol.d.ts +15 -0
  84. package/dist/server/handshake/protocol.js +37 -0
  85. package/dist/server/handshake/readyEmitter.d.ts +6 -0
  86. package/dist/server/handshake/readyEmitter.js +88 -0
  87. package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
  88. package/dist/server/handshake/safetyFallbacks.js +134 -0
  89. package/dist/server/handshake/stdinSniffer.d.ts +1 -0
  90. package/dist/server/handshake/stdinSniffer.js +260 -0
  91. package/dist/server/handshake/tracing.d.ts +16 -0
  92. package/dist/server/handshake/tracing.js +95 -0
  93. package/dist/server/handshakeManager.d.ts +23 -23
  94. package/dist/server/handshakeManager.js +36 -466
  95. package/dist/server/index-server.d.ts +23 -0
  96. package/dist/server/index-server.js +194 -9
  97. package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
  98. package/dist/server/mcpReadOnlySurfaces.js +297 -0
  99. package/dist/server/sdkServer.js +69 -7
  100. package/dist/server/transport.d.ts +5 -6
  101. package/dist/server/transport.js +46 -64
  102. package/dist/server/transportFactory.d.ts +3 -9
  103. package/dist/server/transportFactory.js +18 -380
  104. package/dist/services/atomicFs.d.ts +3 -0
  105. package/dist/services/atomicFs.js +171 -13
  106. package/dist/services/auditLog.d.ts +17 -2
  107. package/dist/services/auditLog.js +75 -14
  108. package/dist/services/bootstrapGating.js +1 -1
  109. package/dist/services/categoryRules.d.ts +10 -0
  110. package/dist/services/categoryRules.js +17 -0
  111. package/dist/services/classificationService.js +7 -5
  112. package/dist/services/embeddingService.d.ts +27 -11
  113. package/dist/services/embeddingService.js +51 -14
  114. package/dist/services/feedbackStorage.d.ts +39 -0
  115. package/dist/services/feedbackStorage.js +88 -0
  116. package/dist/services/handlers/instructions.add.js +429 -317
  117. package/dist/services/handlers/instructions.groom.js +128 -31
  118. package/dist/services/handlers/instructions.import.js +56 -23
  119. package/dist/services/handlers/instructions.patch.js +43 -32
  120. package/dist/services/handlers/instructions.query.js +20 -29
  121. package/dist/services/handlers/instructions.shared.d.ts +54 -0
  122. package/dist/services/handlers/instructions.shared.js +126 -1
  123. package/dist/services/handlers.activation.js +83 -81
  124. package/dist/services/handlers.dashboardConfig.d.ts +2 -2
  125. package/dist/services/handlers.dashboardConfig.js +1 -2
  126. package/dist/services/handlers.diagnostics.js +75 -54
  127. package/dist/services/handlers.feedback.d.ts +4 -11
  128. package/dist/services/handlers.feedback.js +11 -333
  129. package/dist/services/handlers.gates.js +69 -37
  130. package/dist/services/handlers.graph.js +2 -2
  131. package/dist/services/handlers.help.js +2 -2
  132. package/dist/services/handlers.instructionSchema.js +4 -2
  133. package/dist/services/handlers.integrity.js +42 -22
  134. package/dist/services/handlers.messaging.js +1 -1
  135. package/dist/services/handlers.metrics.js +51 -6
  136. package/dist/services/handlers.prompt.js +10 -2
  137. package/dist/services/handlers.search.js +94 -44
  138. package/dist/services/handlers.trace.js +1 -1
  139. package/dist/services/handlers.usage.js +38 -7
  140. package/dist/services/indexContext.d.ts +21 -1
  141. package/dist/services/indexContext.js +263 -78
  142. package/dist/services/indexLoader.d.ts +1 -0
  143. package/dist/services/indexLoader.js +28 -8
  144. package/dist/services/instructionRecordValidation.d.ts +39 -0
  145. package/dist/services/instructionRecordValidation.js +388 -0
  146. package/dist/services/instructions.dispatcher.js +4 -4
  147. package/dist/services/loaderSchemaValidator.d.ts +15 -0
  148. package/dist/services/loaderSchemaValidator.js +69 -0
  149. package/dist/services/logger.js +11 -2
  150. package/dist/services/mcpLogBridge.d.ts +49 -0
  151. package/dist/services/mcpLogBridge.js +83 -0
  152. package/dist/services/ownershipService.js +18 -8
  153. package/dist/services/performanceBaseline.js +23 -22
  154. package/dist/services/promptReviewService.d.ts +3 -1
  155. package/dist/services/promptReviewService.js +41 -13
  156. package/dist/services/regexSafety.d.ts +6 -0
  157. package/dist/services/regexSafety.js +46 -0
  158. package/dist/services/seedBootstrap.js +1 -1
  159. package/dist/services/storage/factory.d.ts +14 -1
  160. package/dist/services/storage/factory.js +61 -1
  161. package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
  162. package/dist/services/storage/jsonEmbeddingStore.js +83 -0
  163. package/dist/services/storage/jsonFileStore.d.ts +3 -1
  164. package/dist/services/storage/jsonFileStore.js +8 -6
  165. package/dist/services/storage/migrationEngine.d.ts +13 -0
  166. package/dist/services/storage/migrationEngine.js +31 -0
  167. package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
  168. package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
  169. package/dist/services/storage/sqliteStore.d.ts +3 -1
  170. package/dist/services/storage/sqliteStore.js +2 -2
  171. package/dist/services/storage/types.d.ts +48 -1
  172. package/dist/services/toolRegistry.js +77 -67
  173. package/dist/services/toolRegistry.zod.js +89 -86
  174. package/dist/services/tracing.js +5 -4
  175. package/dist/utils/envUtils.d.ts +4 -0
  176. package/dist/utils/envUtils.js +7 -0
  177. package/dist/utils/memoryMonitor.js +11 -10
  178. package/package.json +11 -4
  179. package/schemas/instruction.schema.json +38 -1
  180. package/scripts/copy-dashboard-assets.mjs +1 -1
  181. package/scripts/dist/README.md +1 -1
  182. package/scripts/setup-wizard.mjs +781 -0
  183. package/server.json +1 -0
  184. package/dist/externalClientLib.d.ts +0 -1
  185. package/dist/externalClientLib.js +0 -2
  186. package/dist/portableClientWrapper.d.ts +0 -1
  187. package/dist/portableClientWrapper.js +0 -2
  188. package/dist/services/indexingService.d.ts +0 -1
  189. package/dist/services/indexingService.js +0 -2
@@ -18,8 +18,10 @@ const runtimeConfig_1 = require("../../config/runtimeConfig");
18
18
  const canonical_1 = require("../canonical");
19
19
  const manifestManager_1 = require("../manifestManager");
20
20
  const tracing_1 = require("../tracing");
21
+ const instructionRecordValidation_1 = require("../instructionRecordValidation");
22
+ const instructionRecordValidation_2 = require("../instructionRecordValidation");
21
23
  const instructions_shared_1 = require("./instructions.shared");
22
- (0, registry_1.registerHandler)('index_add', (0, instructions_shared_1.guard)('index_add', (p) => {
24
+ (0, registry_1.registerHandler)('index_add', (0, instructions_shared_1.guard)('index_add', async (p) => {
23
25
  const e = p.entry;
24
26
  const instructionsCfg = (0, runtimeConfig_1.getRuntimeConfig)().instructions;
25
27
  const SEMVER_REGEX = /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:[-+].*)?$/;
@@ -38,24 +40,153 @@ const instructions_shared_1 = require("./instructions.shared");
38
40
  } : { id };
39
41
  const base = {
40
42
  id,
43
+ success: false,
41
44
  created: false,
42
45
  overwritten: false,
43
46
  skipped: false,
44
47
  error,
45
48
  hash: opts?.hash,
46
- feedbackHint: 'Creation failed. If unexpected, call feedback_submit with reproEntry.',
49
+ message: opts?.message || 'Instruction not added.',
50
+ feedbackHint: 'Instruction not added. Fix validationErrors or call feedback_submit with reproEntry.',
47
51
  reproEntry
48
52
  };
49
- if (/^missing (entry|id|required fields)/.test(error) || error === 'missing required fields') {
50
- if (ADD_INPUT_SCHEMA) {
51
- base.schemaRef = "meta_tools[name='index_add'].inputSchema";
53
+ if (opts?.validationErrors?.length)
54
+ base.validationErrors = opts.validationErrors;
55
+ if (opts?.hints?.length)
56
+ base.hints = opts.hints;
57
+ if (opts?.errorCode)
58
+ base.errorCode = opts.errorCode;
59
+ if (opts?.detail)
60
+ base.detail = opts.detail;
61
+ if (/^missing (entry|id|required fields)/.test(error) || error === 'missing required fields' || error === 'invalid_instruction') {
62
+ base.schemaRef = instructionRecordValidation_1.INSTRUCTION_INPUT_SCHEMA_REF;
63
+ if (ADD_INPUT_SCHEMA)
52
64
  base.inputSchema = ADD_INPUT_SCHEMA;
65
+ }
66
+ return base;
67
+ };
68
+ const failValidation = (error, validationErrors, hints, opts) => fail(error, { ...opts, validationErrors, hints, message: 'Instruction not added.' });
69
+ const loadExistingEntry = async (id, filePath) => {
70
+ let priorLoad;
71
+ try {
72
+ const stExisting = await (0, indexContext_1.ensureLoadedAsync)();
73
+ const memEntry = stExisting.byId.get(id);
74
+ if (memEntry)
75
+ return { entry: { ...memEntry } };
76
+ }
77
+ catch (err) {
78
+ priorLoad = (0, instructionRecordValidation_1.sanitizeLoadError)(err, 'load_failed');
79
+ }
80
+ if (fs_1.default.existsSync(filePath)) {
81
+ let diskRaw;
82
+ try {
83
+ diskRaw = fs_1.default.readFileSync(filePath, 'utf8');
53
84
  }
54
- else {
55
- base.schemaRef = 'meta_tools (lookup index_add)';
85
+ catch (err) {
86
+ return { error: (0, instructionRecordValidation_1.sanitizeLoadError)(err, 'load_failed') };
87
+ }
88
+ try {
89
+ return { entry: JSON.parse(diskRaw) };
90
+ }
91
+ catch (err) {
92
+ return { error: (0, instructionRecordValidation_1.sanitizeLoadError)(err, 'parse_failed') };
56
93
  }
57
94
  }
58
- return base;
95
+ if (priorLoad)
96
+ return { error: priorLoad };
97
+ return { error: { code: 'unknown', detail: 'missing-existing-entry', raw: 'missing-existing-entry' } };
98
+ };
99
+ const verifyReadBack = async (id, filePath, requestedCategories) => {
100
+ try {
101
+ (0, indexContext_1.invalidate)();
102
+ }
103
+ catch { /* ignore */ }
104
+ const stReloaded = await (0, indexContext_1.ensureLoadedAsync)();
105
+ let strictVerified = false;
106
+ const verifyIssues = [];
107
+ try {
108
+ let parsed;
109
+ parsed = stReloaded.byId.get(id) ?? undefined;
110
+ if (!parsed && fs_1.default.existsSync(filePath)) {
111
+ let diskRaw;
112
+ try {
113
+ diskRaw = fs_1.default.readFileSync(filePath, 'utf8');
114
+ }
115
+ catch (ex) {
116
+ verifyIssues.push('read-failed:' + ex.message);
117
+ }
118
+ if (diskRaw) {
119
+ try {
120
+ parsed = JSON.parse(diskRaw);
121
+ }
122
+ catch (ex) {
123
+ verifyIssues.push('parse-failed:' + ex.message);
124
+ }
125
+ }
126
+ }
127
+ if (parsed) {
128
+ if (parsed.id !== id)
129
+ verifyIssues.push('id-mismatch');
130
+ if (!parsed.title)
131
+ verifyIssues.push('missing-title');
132
+ if (!parsed.body)
133
+ verifyIssues.push('missing-body');
134
+ const wantCats = Array.isArray(requestedCategories)
135
+ ? requestedCategories.filter((c) => typeof c === 'string').map(c => c.toLowerCase())
136
+ : [];
137
+ if (wantCats.length) {
138
+ for (const c of wantCats) {
139
+ if (!parsed.categories?.includes(c))
140
+ verifyIssues.push('missing-category:' + c);
141
+ }
142
+ }
143
+ }
144
+ const mem = stReloaded.byId.get(id);
145
+ if (!mem)
146
+ verifyIssues.push('not-in-index');
147
+ const wantCats2 = Array.isArray(requestedCategories)
148
+ ? requestedCategories.filter((c) => typeof c === 'string').map(c => c.toLowerCase())
149
+ : [];
150
+ if (wantCats2.length) {
151
+ const catHit = stReloaded.list.some(rec => rec.id === id && wantCats2.every(c => rec.categories.includes(c)));
152
+ if (!catHit)
153
+ verifyIssues.push('category-query-miss');
154
+ }
155
+ try {
156
+ if (parsed) {
157
+ const classifier2 = new classificationService_1.ClassificationService();
158
+ const issues = classifier2.validate(parsed);
159
+ if (issues.length)
160
+ verifyIssues.push('classification-issues:' + issues.join(','));
161
+ }
162
+ }
163
+ catch (err) {
164
+ verifyIssues.push('classification-exception:' + err.message);
165
+ }
166
+ if (verifyIssues.includes('not-in-index')) {
167
+ try {
168
+ (0, indexContext_1.invalidate)();
169
+ const st2 = await (0, indexContext_1.ensureLoadedAsync)();
170
+ if (st2.byId.has(id)) {
171
+ const idx = verifyIssues.indexOf('not-in-index');
172
+ if (idx >= 0)
173
+ verifyIssues.splice(idx, 1);
174
+ }
175
+ }
176
+ catch { /* ignore */ }
177
+ }
178
+ if (!verifyIssues.length)
179
+ strictVerified = true;
180
+ }
181
+ catch (err) {
182
+ verifyIssues.push('verify-exception:' + err.message);
183
+ }
184
+ return {
185
+ stReloaded,
186
+ strictVerified,
187
+ verifyIssues,
188
+ verified: strictVerified && verifyIssues.length === 0,
189
+ };
59
190
  };
60
191
  const metadataEquals = (left, right) => {
61
192
  if (left === right)
@@ -79,87 +210,60 @@ const instructions_shared_1 = require("./instructions.shared");
79
210
  if (!e.id)
80
211
  return fail('missing id');
81
212
  const mutable = e;
82
- if (!mutable.title)
213
+ // Only fill defaults for *missing* fields. Never silently coerce a wrong-typed value:
214
+ // shape violations are surfaced as invalid_instruction by validateInstructionInputSurface.
215
+ if (mutable.title === undefined)
83
216
  mutable.title = mutable.id;
84
- if (typeof mutable.priority !== 'number')
217
+ if (mutable.priority === undefined)
85
218
  mutable.priority = 50;
86
- if (!mutable.audience)
219
+ if (mutable.audience === undefined)
87
220
  mutable.audience = 'all';
88
- if (!mutable.requirement)
221
+ if (mutable.requirement === undefined)
89
222
  mutable.requirement = 'optional';
90
- if (!Array.isArray(mutable.categories))
223
+ if (mutable.categories === undefined)
91
224
  mutable.categories = [];
92
225
  }
93
- if (p.overwrite && (!e.body || !e.title)) {
94
- try {
95
- // Try in-memory state first (covers SQLite and JSON backends), fall back to disk
96
- let raw;
97
- const stHydrate = (0, indexContext_1.ensureLoaded)();
98
- const memEntry = stHydrate.byId.get(e.id);
99
- if (memEntry) {
100
- raw = { ...memEntry };
101
- }
102
- if (!raw) {
103
- const dirCandidate = (0, indexContext_1.getInstructionsDir)();
104
- const fileCandidate = path_1.default.join(dirCandidate, `${e.id}.json`);
105
- if (fs_1.default.existsSync(fileCandidate)) {
106
- try {
107
- raw = JSON.parse(fs_1.default.readFileSync(fileCandidate, 'utf8'));
108
- }
109
- catch { /* ignore parse */ }
110
- }
111
- }
112
- if (raw) {
113
- const mutableExisting = e;
114
- if (!mutableExisting.body && typeof raw.body === 'string' && raw.body.trim()) {
115
- mutableExisting.body = raw.body;
116
- }
117
- if (!mutableExisting.title && typeof raw.title === 'string' && raw.title.trim()) {
118
- mutableExisting.title = raw.title;
119
- }
120
- }
121
- }
122
- catch { /* ignore hydration errors */ }
123
- }
124
- if (!e.id || !e.title || !e.body)
125
- return fail('missing required fields');
126
226
  const dir = (0, indexContext_1.getInstructionsDir)();
127
227
  if (!fs_1.default.existsSync(dir))
128
228
  fs_1.default.mkdirSync(dir, { recursive: true });
129
229
  const file = path_1.default.join(dir, `${e.id}.json`);
130
- const existsInStore = (0, indexContext_1.ensureLoaded)().byId.has(e.id);
131
- const existsOnDisk = !existsInStore && fs_1.default.existsSync(file);
132
- const exists = existsInStore || existsOnDisk;
133
- const existedBeforeOriginal = exists;
134
- const overwrite = !!p.overwrite;
135
- if (exists && !overwrite) {
136
- let st0 = (0, indexContext_1.ensureLoaded)();
137
- let visible = st0.byId.has(e.id);
138
- let repaired = false;
139
- if (!visible) {
140
- try {
141
- (0, indexContext_1.invalidate)();
142
- st0 = (0, indexContext_1.ensureLoaded)();
143
- visible = st0.byId.has(e.id);
144
- if (visible)
145
- repaired = true;
230
+ if (p.overwrite && (!e.body || !e.title)) {
231
+ const hydrated = await loadExistingEntry(e.id, file);
232
+ if (hydrated.entry) {
233
+ const mutableExisting = e;
234
+ if (!mutableExisting.body && typeof hydrated.entry.body === 'string' && hydrated.entry.body.trim()) {
235
+ mutableExisting.body = hydrated.entry.body;
236
+ }
237
+ if (!mutableExisting.title && typeof hydrated.entry.title === 'string' && hydrated.entry.title.trim()) {
238
+ mutableExisting.title = hydrated.entry.title;
146
239
  }
147
- catch { /* ignore reload */ }
148
- }
149
- (0, auditLog_1.logAudit)('add', e.id, { skipped: true, late_visible: visible, repaired });
150
- if ((0, instructions_shared_1.traceVisibility)()) {
151
- (0, tracing_1.emitTrace)('[trace:add:skip]', { id: e.id, visible, repaired });
152
- }
153
- if ((0, instructions_shared_1.traceVisibility)()) {
154
- (0, instructions_shared_1.traceInstructionVisibility)(e.id, 'add-skip-pre-return', { visible, repaired });
155
- if (!visible)
156
- (0, instructions_shared_1.traceEnvSnapshot)('add-skip-anomalous', { repaired });
157
240
  }
158
- if (!visible) {
159
- return { id: e.id, skipped: true, created: false, overwritten: false, hash: st0.hash, visibilityWarning: 'skipped_file_not_in_index' };
241
+ else if (hydrated.error) {
242
+ // Surface all read failures, including those combined with 'missing-existing-entry'
243
+ const hasRealError = hydrated.error.detail !== 'missing-existing-entry';
244
+ if (hasRealError) {
245
+ (0, auditLog_1.logAudit)('add_hydration_error', e.id, { error: hydrated.error.raw, errorCode: hydrated.error.code, overwrite: true });
246
+ return fail('existing_instruction_unreadable', {
247
+ id: e.id,
248
+ message: `Existing instruction could not be hydrated for overwrite (${hydrated.error.code}): ${hydrated.error.detail}`,
249
+ errorCode: hydrated.error.code,
250
+ detail: hydrated.error.detail,
251
+ });
252
+ }
160
253
  }
161
- return { id: e.id, skipped: true, created: false, overwritten: false, hash: st0.hash, repaired: repaired ? true : undefined };
162
254
  }
255
+ const requiredFieldErrors = [
256
+ !e.id ? 'id: missing required field' : undefined,
257
+ e.title === undefined ? 'title: missing required field' : undefined,
258
+ e.body === undefined ? 'body: missing required field' : undefined,
259
+ ].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 });
263
+ }
264
+ const overwrite = !!p.overwrite;
265
+ const exists = overwrite ? ((await (0, indexContext_1.ensureLoadedAsync)()).byId.has(e.id) || fs_1.default.existsSync(file)) : false;
266
+ const existedBeforeOriginal = exists;
163
267
  const now = new Date().toISOString();
164
268
  const rawBody = typeof e.body === 'string' ? e.body : String(e.body || '');
165
269
  const bodyTrimmed = rawBody.trim();
@@ -172,7 +276,7 @@ const instructions_shared_1 = require("./instructions.shared");
172
276
  guidance: `Body exceeds the ${bodyWarnLength}-character limit (${bodyTrimmed.length} chars). Please split into multiple cross-linked instructions, refine/compress content, or categorize sections as separate entries. Use categories and cross-references (e.g., "See also: <sibling-id>") to maintain discoverability.`
173
277
  };
174
278
  }
175
- let categories = Array.from(new Set((Array.isArray(e.categories) ? e.categories : []).filter((c) => typeof c === 'string' && c.trim().length > 0).map(c => c.toLowerCase()))).sort();
279
+ let categories = (0, instructions_shared_1.normalizeInputCategories)(e.categories);
176
280
  if (!categories.length) {
177
281
  const allowAuto = lax || !instructionsCfg.requireCategory;
178
282
  if (allowAuto) {
@@ -197,130 +301,154 @@ const instructions_shared_1 = require("./instructions.shared");
197
301
  const classifier = new classificationService_1.ClassificationService();
198
302
  let base;
199
303
  if (exists) {
200
- try {
201
- let existing;
202
- // Try store first (covers SQLite), fall back to disk (JSON backend)
203
- const stMerge = (0, indexContext_1.ensureLoaded)();
204
- const memEntry = stMerge.byId.get(e.id);
205
- if (memEntry) {
206
- existing = { ...memEntry };
207
- }
208
- else if (existsOnDisk) {
209
- existing = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
210
- }
211
- else {
212
- throw new Error('entry not found in store or on disk');
213
- }
214
- base = { ...existing };
215
- const prevBody = existing.body;
216
- const prevVersion = existing.version || '1.0.0';
217
- if (e.title)
218
- base.title = e.title;
219
- if (e.body)
220
- base.body = bodyTrimmed;
221
- if (e.rationale !== undefined)
222
- base.rationale = e.rationale;
223
- if (typeof e.priority === 'number')
224
- base.priority = e.priority;
225
- if (e.audience)
226
- base.audience = e.audience;
227
- if (e.requirement)
228
- base.requirement = e.requirement;
229
- if (categories.length) {
230
- base.categories = categories;
231
- base.primaryCategory = primaryCategory;
232
- }
233
- base.updatedAt = now;
234
- if (e.version !== undefined)
235
- base.version = e.version;
236
- if (e.changeLog !== undefined)
237
- base.changeLog = e.changeLog;
238
- const semverRegex = SEMVER_REGEX;
239
- const parse = (v) => { const m = semverRegex.exec(v); if (!m)
240
- return null; return { major: +m[1], minor: +m[2], patch: +m[3] }; };
241
- const gt = (a, b) => { const pa = parse(a), pb = parse(b); if (!pa || !pb)
242
- return false; if (pa.major !== pb.major)
243
- return pa.major > pb.major; if (pa.minor !== pb.minor)
244
- return pa.minor > pb.minor; return pa.patch > pb.patch; };
245
- const bodyChanged = e.body ? (bodyTrimmed !== prevBody) : false;
246
- const titleChanged = e.title !== undefined && e.title !== existing.title;
247
- const eRec = e;
248
- const mutableMetadataKeys = ['owner', 'status', 'priorityTier', 'classification', 'lastReviewedAt', 'nextReviewDue', 'semanticSummary', 'contentType', 'extensions'];
249
- const metadataChanged = mutableMetadataKeys.some((key) => eRec[key] !== undefined && !metadataEquals(eRec[key], existing[key]));
250
- const versionChanged = e.version !== undefined && e.version !== existing.version;
251
- const categoriesChanged = categories.length > 0 && JSON.stringify(categories.sort()) !== JSON.stringify((existing.categories || []).sort());
252
- const governanceMetaChanged = titleChanged || metadataChanged || versionChanged || categoriesChanged;
253
- if (overwrite && !bodyChanged && !governanceMetaChanged) {
254
- const stNoop = (0, indexContext_1.ensureLoaded)();
255
- const respNoop = { id: e.id, created: false, overwritten: false, skipped: true, hash: stNoop.hash, verified: true };
256
- if (instructionsCfg.strictCreate)
257
- respNoop.strictVerified = true;
258
- (0, auditLog_1.logAudit)('add', e.id, { created: false, overwritten: false, skipped: true, verified: true, noop: true });
259
- if ((0, instructions_shared_1.traceVisibility)())
260
- (0, tracing_1.emitTrace)('[trace:add:noop-overwrite]', { id: e.id, hash: stNoop.hash, reason: 'no body/governance delta' });
261
- return respNoop;
304
+ const existingLoad = await loadExistingEntry(e.id, file);
305
+ if (!existingLoad.entry) {
306
+ const errCode = existingLoad.error?.code ?? 'unknown';
307
+ const errDetail = existingLoad.error?.detail ?? 'missing-existing-entry';
308
+ return fail('existing_instruction_unreadable', {
309
+ id: e.id,
310
+ message: `Existing instruction could not be read for overwrite (${errCode}): ${errDetail}`,
311
+ errorCode: errCode,
312
+ detail: errDetail,
313
+ });
314
+ }
315
+ const existing = existingLoad.entry;
316
+ base = { ...existing };
317
+ const prevBody = existing.body;
318
+ const prevVersion = existing.version || '1.0.0';
319
+ if (e.title)
320
+ base.title = e.title;
321
+ if (e.body)
322
+ base.body = bodyTrimmed;
323
+ if (e.rationale !== undefined)
324
+ base.rationale = e.rationale;
325
+ if (typeof e.priority === 'number')
326
+ base.priority = e.priority;
327
+ if (e.audience)
328
+ base.audience = e.audience;
329
+ if (e.requirement)
330
+ base.requirement = e.requirement;
331
+ if (categories.length) {
332
+ base.categories = categories;
333
+ base.primaryCategory = primaryCategory;
334
+ }
335
+ base.updatedAt = now;
336
+ if (e.version !== undefined)
337
+ base.version = e.version;
338
+ if (e.changeLog !== undefined)
339
+ base.changeLog = e.changeLog;
340
+ const semverRegex = SEMVER_REGEX;
341
+ const parse = (v) => { const m = semverRegex.exec(v); if (!m)
342
+ return null; return { major: +m[1], minor: +m[2], patch: +m[3] }; };
343
+ const gt = (a, b) => { const pa = parse(a), pb = parse(b); if (!pa || !pb)
344
+ return false; if (pa.major !== pb.major)
345
+ return pa.major > pb.major; if (pa.minor !== pb.minor)
346
+ return pa.minor > pb.minor; return pa.patch > pb.patch; };
347
+ const bodyChanged = e.body ? (bodyTrimmed !== prevBody) : false;
348
+ const titleChanged = e.title !== undefined && e.title !== existing.title;
349
+ const eRec = e;
350
+ const mutableMetadataKeys = ['owner', 'status', 'priorityTier', 'classification', 'lastReviewedAt', 'nextReviewDue', 'semanticSummary', 'contentType', 'extensions'];
351
+ const metadataChanged = mutableMetadataKeys.some((key) => eRec[key] !== undefined && !metadataEquals(eRec[key], existing[key]));
352
+ const versionChanged = e.version !== undefined && e.version !== existing.version;
353
+ const categoriesChanged = categories.length > 0 && JSON.stringify(categories.sort()) !== JSON.stringify((existing.categories || []).sort());
354
+ const governanceMetaChanged = titleChanged || metadataChanged || versionChanged || categoriesChanged;
355
+ if (overwrite && !bodyChanged && !governanceMetaChanged) {
356
+ // Noop overwrite: no mutation needed because the incoming entry matches
357
+ // the existing record. Even so, verify the persisted state still matches
358
+ // the in-memory index — the file (or store row) may have disappeared
359
+ // since the index was last loaded. Skipping verification here would mean
360
+ // silently reporting success when the entry is no longer durable.
361
+ const verification = await verifyReadBack(e.id, file, e.categories);
362
+ const noopVerified = verification.verified;
363
+ (0, auditLog_1.logAudit)('add', e.id, {
364
+ created: false,
365
+ overwritten: false,
366
+ skipped: true,
367
+ verified: noopVerified,
368
+ strictVerified: verification.strictVerified,
369
+ verifyIssues: verification.verifyIssues.length ? verification.verifyIssues : undefined,
370
+ noop: true,
371
+ note: noopVerified ? 'noop_verified' : 'noop_read_back_failed',
372
+ });
373
+ if ((0, instructions_shared_1.traceVisibility)())
374
+ (0, tracing_1.emitTrace)('[trace:add:noop-overwrite]', {
375
+ id: e.id,
376
+ hash: verification.stReloaded.hash,
377
+ verified: noopVerified,
378
+ strictVerified: verification.strictVerified,
379
+ issues: verification.verifyIssues.slice(0, 5),
380
+ reason: noopVerified
381
+ ? 'no body/governance delta — persisted state verified'
382
+ : 'no body/governance delta — persisted state verification failed',
383
+ });
384
+ if (!noopVerified) {
385
+ return {
386
+ ...fail('read-back verification failed', {
387
+ id: e.id,
388
+ hash: verification.stReloaded.hash,
389
+ message: 'Noop overwrite rejected: persisted instruction state does not match the in-memory index.',
390
+ validationErrors: verification.verifyIssues,
391
+ }),
392
+ created: false,
393
+ overwritten: false,
394
+ verified: false,
395
+ strictVerified: verification.strictVerified,
396
+ verifyIssues: verification.verifyIssues,
397
+ };
262
398
  }
263
- let incomingVersion = e.version;
264
- if (incomingVersion && !semverRegex.test(incomingVersion))
265
- return fail('invalid_semver', { id: e.id });
266
- if (bodyChanged) {
267
- if (incomingVersion) {
268
- if (!gt(incomingVersion, prevVersion))
269
- return fail('version_not_bumped', { id: e.id });
270
- }
271
- else {
272
- const pv = parse(prevVersion) || { major: 1, minor: 0, patch: 0 };
273
- const autoVersion = `${pv.major}.${pv.minor}.${pv.patch + 1}`;
274
- base.version = autoVersion;
275
- incomingVersion = autoVersion;
276
- }
399
+ const respNoop = {
400
+ id: e.id,
401
+ success: true,
402
+ created: false,
403
+ overwritten: false,
404
+ skipped: true,
405
+ hash: verification.stReloaded.hash,
406
+ verified: true,
407
+ note: 'noop_verified',
408
+ };
409
+ if (instructionsCfg.strictCreate) {
410
+ respNoop.strictVerified = verification.strictVerified;
277
411
  }
278
- else if (incomingVersion) {
412
+ return respNoop;
413
+ }
414
+ let incomingVersion = e.version;
415
+ if (incomingVersion && !semverRegex.test(incomingVersion))
416
+ return fail('invalid_semver', { id: e.id });
417
+ if (bodyChanged) {
418
+ if (incomingVersion) {
279
419
  if (!gt(incomingVersion, prevVersion))
280
420
  return fail('version_not_bumped', { id: e.id });
281
421
  }
282
422
  else {
283
- base.version = prevVersion;
284
- incomingVersion = prevVersion;
285
- }
286
- if (!Array.isArray(base.changeLog) || !base.changeLog.length) {
287
- base.changeLog = [{ version: prevVersion, changedAt: existing.createdAt || now, summary: 'initial import' }];
288
- }
289
- const finalVersion = base.version || incomingVersion || prevVersion;
290
- const last = base.changeLog[base.changeLog.length - 1];
291
- if (last.version !== finalVersion) {
292
- const summary = bodyChanged ? (e.version ? 'body update' : 'auto bump (body change)') : 'metadata update';
293
- base.changeLog.push({ version: finalVersion, changedAt: now, summary });
423
+ const pv = parse(prevVersion) || { major: 1, minor: 0, patch: 0 };
424
+ const autoVersion = `${pv.major}.${pv.minor}.${pv.patch + 1}`;
425
+ base.version = autoVersion;
426
+ incomingVersion = autoVersion;
294
427
  }
295
- const repairChangeLog = (cl) => {
296
- const out = [];
297
- if (Array.isArray(cl)) {
298
- for (const entry of cl) {
299
- if (!entry || typeof entry !== 'object')
300
- continue;
301
- const { version: v, changedAt: ca, summary: sum } = entry;
302
- if (typeof v === 'string' && v.trim() && typeof sum === 'string' && sum.trim()) {
303
- const caIso = typeof ca === 'string' && /T/.test(ca) ? ca : now;
304
- out.push({ version: v.trim(), changedAt: caIso, summary: sum.trim() });
305
- }
306
- }
307
- }
308
- if (!out.length) {
309
- out.push({ version: prevVersion, changedAt: existing.createdAt || now, summary: 'initial import (repaired)' });
310
- }
311
- const lastVer = out[out.length - 1].version;
312
- if (lastVer !== finalVersion) {
313
- out.push({ version: finalVersion, changedAt: now, summary: bodyChanged ? 'body update (repaired)' : 'metadata update (repaired)' });
314
- }
315
- return out;
316
- };
317
- base.changeLog = repairChangeLog(base.changeLog);
318
428
  }
319
- catch {
320
- base = { id: e.id, title: e.title, body: bodyTrimmed, rationale: e.rationale, priority: e.priority, audience: e.audience, requirement: e.requirement, categories, primaryCategory, sourceHash, schemaVersion: schemaVersion_1.SCHEMA_VERSION, deprecatedBy: e.deprecatedBy, createdAt: now, updatedAt: now, riskScore: e.riskScore, createdByAgent: instructionsCfg.agentId, sourceWorkspace: instructionsCfg.workspaceId, extensions: e.extensions };
321
- base.version = '1.0.0';
322
- base.changeLog = [{ version: '1.0.0', changedAt: now, summary: 'initial import' }];
429
+ else if (incomingVersion) {
430
+ if (!gt(incomingVersion, prevVersion))
431
+ return fail('version_not_bumped', { id: e.id });
432
+ }
433
+ else {
434
+ base.version = prevVersion;
435
+ incomingVersion = prevVersion;
436
+ }
437
+ if (!Array.isArray(base.changeLog) || !base.changeLog.length) {
438
+ base.changeLog = [{ version: prevVersion, changedAt: existing.createdAt || now, summary: 'initial import' }];
323
439
  }
440
+ const finalVersion = base.version || incomingVersion || prevVersion;
441
+ const last = base.changeLog[base.changeLog.length - 1];
442
+ if (last.version !== finalVersion) {
443
+ const summary = bodyChanged ? (e.version ? 'body update' : 'auto bump (body change)') : 'metadata update';
444
+ base.changeLog.push({ version: finalVersion, changedAt: now, summary });
445
+ }
446
+ base.changeLog = (0, instructions_shared_1.repairChangeLog)(base.changeLog, {
447
+ finalVersion,
448
+ now,
449
+ fallback: { version: prevVersion, changedAt: existing.createdAt || now, summary: 'initial import (repaired)' },
450
+ trailingSummary: bodyChanged ? 'body update (repaired)' : 'metadata update (repaired)'
451
+ });
324
452
  }
325
453
  else {
326
454
  base = { id: e.id, title: e.title, body: bodyTrimmed, rationale: e.rationale, priority: e.priority, audience: e.audience, requirement: e.requirement, categories, primaryCategory, sourceHash, schemaVersion: schemaVersion_1.SCHEMA_VERSION, deprecatedBy: e.deprecatedBy, createdAt: now, updatedAt: now, riskScore: e.riskScore, createdByAgent: instructionsCfg.agentId, sourceWorkspace: instructionsCfg.workspaceId, extensions: e.extensions };
@@ -336,32 +464,15 @@ const instructions_shared_1 = require("./instructions.shared");
336
464
  base.changeLog = [{ version: base.version, changedAt: now, summary: 'initial import' }];
337
465
  }
338
466
  if (Array.isArray(base.changeLog)) {
339
- const repaired = [];
340
- for (const entry of base.changeLog) {
341
- if (!entry || typeof entry !== 'object')
342
- continue;
343
- const { version: v, changedAt: ca, summary: sum } = entry;
344
- if (typeof v === 'string' && v.trim() && typeof sum === 'string' && sum.trim()) {
345
- const caIso = typeof ca === 'string' && /T/.test(ca) ? ca : now;
346
- repaired.push({ version: v.trim(), changedAt: caIso, summary: sum.trim() });
347
- }
348
- }
349
- if (!repaired.length) {
350
- repaired.push({ version: base.version, changedAt: now, summary: 'initial import (repaired)' });
351
- }
352
- if (repaired[repaired.length - 1].version !== base.version) {
353
- repaired.push({ version: base.version, changedAt: now, summary: 'initial import (normalized)' });
354
- }
355
- base.changeLog = repaired;
356
- }
357
- }
358
- const govKeys = ['version', 'owner', 'status', 'priorityTier', 'classification', 'lastReviewedAt', 'nextReviewDue', 'semanticSummary', 'contentType', 'extensions'];
359
- for (const k of govKeys) {
360
- const v = e[k];
361
- if (v !== undefined) {
362
- base[k] = v;
467
+ base.changeLog = (0, instructions_shared_1.repairChangeLog)(base.changeLog, {
468
+ finalVersion: base.version,
469
+ now,
470
+ fallback: { version: base.version, changedAt: now, summary: 'initial import (repaired)' },
471
+ trailingSummary: 'initial import (normalized)'
472
+ });
363
473
  }
364
474
  }
475
+ (0, instructions_shared_1.applyGovernanceKeys)(base, e, instructions_shared_1.ADD_GOVERNANCE_KEYS);
365
476
  if (!base.sourceWorkspace)
366
477
  base.sourceWorkspace = instructionsCfg.workspaceId;
367
478
  if (!exists || base.body === bodyTrimmed) {
@@ -380,119 +491,75 @@ const instructions_shared_1 = require("./instructions.shared");
380
491
  record.updatedAt = new Date().toISOString();
381
492
  }
382
493
  }
383
- try {
384
- (0, indexContext_1.writeEntry)(record);
385
- }
386
- catch (err) {
387
- return fail(err.message || 'write-failed', { id: e.id });
494
+ const recordValidation = (0, instructionRecordValidation_1.validateInstructionRecord)(record);
495
+ if (recordValidation.validationErrors.length) {
496
+ return failValidation('invalid_instruction', recordValidation.validationErrors, recordValidation.hints, { id: e.id });
388
497
  }
389
498
  try {
390
- (0, indexContext_1.touchIndexVersion)();
391
- }
392
- catch { /* ignore */ }
393
- let stReloaded;
394
- const strictMode = instructionsCfg.strictVisibility;
395
- if (strictMode) {
396
- try {
397
- const current = (0, indexContext_1.ensureLoaded)();
398
- stReloaded = current;
399
- if (!current.byId.has(record.id)) {
400
- current.byId.set(record.id, record);
401
- current.list.push(record);
402
- }
403
- }
404
- catch { /* fallback to reload path below if anything fails */ }
499
+ await (0, indexContext_1.writeEntryAsync)(record, overwrite ? undefined : { createOnly: true });
405
500
  }
406
- if (!stReloaded) {
407
- try {
408
- (0, indexContext_1.invalidate)();
501
+ catch (err) {
502
+ if ((0, instructionRecordValidation_2.isInstructionValidationError)(err)) {
503
+ return failValidation('invalid_instruction', err.validationErrors, err.hints, { id: e.id });
409
504
  }
410
- catch { /* ignore */ }
411
- stReloaded = (0, indexContext_1.ensureLoaded)();
412
- }
413
- const createdNow = !existedBeforeOriginal;
414
- const overwrittenNow = overwrite && existedBeforeOriginal;
415
- let strictVerified = false;
416
- const verifyIssues = [];
417
- try {
418
- let parsed;
419
- // Verify from in-memory store first (covers SQLite), fall back to disk (JSON backend)
420
- parsed = stReloaded.byId.get(e.id) ?? undefined;
421
- if (!parsed) {
422
- if (fs_1.default.existsSync(file)) {
423
- let diskRaw;
505
+ if (!overwrite && (0, indexContext_1.isDuplicateInstructionWriteError)(err)) {
506
+ let st0 = await (0, indexContext_1.ensureLoadedAsync)();
507
+ let visible = st0.byId.has(e.id);
508
+ let repaired = false;
509
+ if (!visible) {
424
510
  try {
425
- diskRaw = fs_1.default.readFileSync(file, 'utf8');
426
- }
427
- catch (ex) {
428
- verifyIssues.push('read-failed:' + ex.message);
429
- }
430
- if (diskRaw) {
431
- try {
432
- parsed = JSON.parse(diskRaw);
433
- }
434
- catch (ex) {
435
- verifyIssues.push('parse-failed:' + ex.message);
436
- }
511
+ (0, indexContext_1.invalidate)();
512
+ st0 = await (0, indexContext_1.ensureLoadedAsync)();
513
+ visible = st0.byId.has(e.id);
514
+ if (visible)
515
+ repaired = true;
437
516
  }
517
+ catch { /* ignore reload */ }
438
518
  }
439
- }
440
- if (parsed) {
441
- if (parsed.id !== e.id)
442
- verifyIssues.push('id-mismatch');
443
- if (!parsed.title)
444
- verifyIssues.push('missing-title');
445
- if (!parsed.body)
446
- verifyIssues.push('missing-body');
447
- const wantCats = Array.isArray(e.categories) ? e.categories.filter((c) => typeof c === 'string').map(c => c.toLowerCase()) : [];
448
- if (wantCats.length) {
449
- for (const c of wantCats) {
450
- if (!parsed.categories?.includes(c)) {
451
- verifyIssues.push('missing-category:' + c);
452
- }
453
- }
519
+ (0, auditLog_1.logAudit)('add', e.id, { skipped: true, late_visible: visible, repaired, duplicateAtWrite: true });
520
+ if ((0, instructions_shared_1.traceVisibility)()) {
521
+ (0, tracing_1.emitTrace)('[trace:add:skip]', { id: e.id, visible, repaired, duplicateAtWrite: true });
454
522
  }
455
- }
456
- const mem = stReloaded.byId.get(e.id);
457
- if (!mem) {
458
- verifyIssues.push('not-in-index');
459
- }
460
- const wantCats2 = Array.isArray(e.categories) ? e.categories.filter((c) => typeof c === 'string').map(c => c.toLowerCase()) : [];
461
- if (wantCats2.length) {
462
- const catHit = stReloaded.list.some(rec => rec.id === e.id && wantCats2.every(c => rec.categories.includes(c)));
463
- if (!catHit)
464
- verifyIssues.push('category-query-miss');
465
- }
466
- try {
467
- if (parsed) {
468
- const classifier2 = new classificationService_1.ClassificationService();
469
- const issues = classifier2.validate(parsed);
470
- if (issues.length) {
471
- verifyIssues.push('classification-issues:' + issues.join(','));
472
- }
523
+ if ((0, instructions_shared_1.traceVisibility)()) {
524
+ (0, instructions_shared_1.traceInstructionVisibility)(e.id, 'add-skip-post-write-conflict', { visible, repaired });
525
+ if (!visible)
526
+ (0, instructions_shared_1.traceEnvSnapshot)('add-skip-anomalous', { repaired });
473
527
  }
474
- }
475
- catch (err) {
476
- verifyIssues.push('classification-exception:' + err.message);
477
- }
478
- if (verifyIssues.includes('not-in-index')) {
479
- try {
480
- (0, indexContext_1.invalidate)();
481
- const st2 = (0, indexContext_1.ensureLoaded)();
482
- if (st2.byId.has(e.id)) {
483
- const idx = verifyIssues.indexOf('not-in-index');
484
- if (idx >= 0)
485
- verifyIssues.splice(idx, 1);
528
+ if (!visible) {
529
+ const existingLoadError = st0.loadErrors?.find((issue) => {
530
+ const fileName = path_1.default.basename(issue.file);
531
+ return issue.file === `${e.id}.json` || fileName === `${e.id}.json` || issue.file.endsWith(`\\${e.id}.json`);
532
+ });
533
+ if (existingLoadError) {
534
+ return {
535
+ id: e.id,
536
+ success: false,
537
+ skipped: false,
538
+ created: false,
539
+ overwritten: false,
540
+ hash: st0.hash,
541
+ error: 'existing_instruction_invalid',
542
+ validationErrors: [(0, instructionRecordValidation_1.sanitizeErrorDetail)(existingLoadError.error) || 'existing entry could not be parsed'],
543
+ };
486
544
  }
545
+ return { id: e.id, success: true, skipped: true, created: false, overwritten: false, hash: st0.hash, visibilityWarning: 'skipped_file_not_in_index' };
487
546
  }
488
- catch { /* ignore */ }
547
+ return { id: e.id, success: true, skipped: true, created: false, overwritten: false, hash: st0.hash, repaired: repaired ? true : undefined };
489
548
  }
490
- if (!verifyIssues.length)
491
- strictVerified = true;
549
+ // Catch-all: never expose raw Node error text (ENOENT, null-byte path errors, stack frames) to MCP clients.
550
+ return fail('write_failed', {
551
+ id: e.id,
552
+ message: 'Instruction write failed due to an internal error. The error details are not exposed to clients.',
553
+ });
492
554
  }
493
- catch (err) {
494
- verifyIssues.push('verify-exception:' + err.message);
555
+ try {
556
+ (0, indexContext_1.touchIndexVersion)();
495
557
  }
558
+ catch { /* ignore */ }
559
+ const strictMode = instructionsCfg.strictVisibility || instructionsCfg.strictCreate;
560
+ const createdNow = !existedBeforeOriginal;
561
+ const overwrittenNow = overwrite && existedBeforeOriginal;
562
+ const verification = await verifyReadBack(e.id, file, e.categories);
496
563
  try {
497
564
  if (instructionsCfg.manifest.writeEnabled)
498
565
  (0, manifestManager_1.writeManifestFromIndex)();
@@ -503,8 +570,53 @@ const instructions_shared_1 = require("./instructions.shared");
503
570
  catch { /* ignore */ } });
504
571
  }
505
572
  catch { /* ignore manifest */ }
506
- (0, auditLog_1.logAudit)('add', e.id, { created: createdNow, overwritten: overwrittenNow, verified: true, forcedReload: true });
573
+ (0, auditLog_1.logAudit)('add', e.id, {
574
+ created: createdNow,
575
+ overwritten: overwrittenNow,
576
+ verified: verification.verified,
577
+ strictVerified: verification.strictVerified,
578
+ verifyIssues: verification.verifyIssues.length ? verification.verifyIssues : undefined,
579
+ forcedReload: true,
580
+ });
507
581
  if ((0, instructions_shared_1.traceVisibility)())
508
- (0, tracing_1.emitTrace)('[trace:add:forced-reload]', { id: e.id, created: createdNow, overwritten: overwrittenNow, hash: stReloaded.hash, strictVerified, issues: verifyIssues.slice(0, 5), strictMode });
509
- return { id: e.id, created: createdNow, overwritten: overwrittenNow, skipped: false, hash: stReloaded.hash, verified: true, strictVerified, verifyIssues: verifyIssues.length ? verifyIssues : undefined, strictMode, bodyLength: bodyTrimmed.length };
582
+ (0, tracing_1.emitTrace)('[trace:add:forced-reload]', {
583
+ id: e.id,
584
+ created: createdNow,
585
+ overwritten: overwrittenNow,
586
+ hash: verification.stReloaded.hash,
587
+ verified: verification.verified,
588
+ strictVerified: verification.strictVerified,
589
+ issues: verification.verifyIssues.slice(0, 5),
590
+ strictMode,
591
+ });
592
+ if (!verification.verified) {
593
+ return {
594
+ ...fail('read-back verification failed', {
595
+ id: e.id,
596
+ hash: verification.stReloaded.hash,
597
+ message: 'Instruction write completed but read-back verification failed.',
598
+ validationErrors: verification.verifyIssues,
599
+ }),
600
+ created: createdNow,
601
+ overwritten: overwrittenNow,
602
+ verified: false,
603
+ strictVerified: verification.strictVerified,
604
+ verifyIssues: verification.verifyIssues,
605
+ strictMode,
606
+ bodyLength: bodyTrimmed.length,
607
+ };
608
+ }
609
+ return {
610
+ id: e.id,
611
+ success: true,
612
+ created: createdNow,
613
+ overwritten: overwrittenNow,
614
+ skipped: false,
615
+ hash: verification.stReloaded.hash,
616
+ verified: verification.verified,
617
+ strictVerified: verification.strictVerified,
618
+ verifyIssues: undefined,
619
+ strictMode,
620
+ bodyLength: bodyTrimmed.length,
621
+ };
510
622
  }));