hadara 0.3.2 → 0.3.3

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 (52) hide show
  1. package/README.md +49 -34
  2. package/dist/cli/context.js +110 -0
  3. package/dist/cli/help.js +6 -7
  4. package/dist/cli/init.js +56 -58
  5. package/dist/cli/main.js +12 -0
  6. package/dist/cli/session.js +37 -0
  7. package/dist/cli/task.js +52 -0
  8. package/dist/context/code-graph-extractor.js +123 -0
  9. package/dist/context/code-index.js +1154 -0
  10. package/dist/context/context-cache-store.js +925 -0
  11. package/dist/context/context-graph-builder.js +341 -0
  12. package/dist/context/context-graph.js +42 -0
  13. package/dist/context/context-pack.js +457 -0
  14. package/dist/context/context-slice-boundary.js +37 -0
  15. package/dist/context/context-slice.js +487 -0
  16. package/dist/context/document-extractors.js +343 -0
  17. package/dist/context/evidence-extractors.js +179 -0
  18. package/dist/context/extractor-contract.js +166 -0
  19. package/dist/context/registry-extractors.js +177 -0
  20. package/dist/context/release-extractors.js +175 -0
  21. package/dist/context/session-start.js +297 -0
  22. package/dist/context/source-manifest.js +566 -0
  23. package/dist/context/state-projection.js +209 -0
  24. package/dist/context/task-extractors.js +168 -0
  25. package/dist/core/schema.js +26 -0
  26. package/dist/harness/validate.js +7 -8
  27. package/dist/schemas/code-index.schema.json +173 -0
  28. package/dist/schemas/context-cache-record.schema.json +56 -0
  29. package/dist/schemas/context-cache-status.schema.json +167 -0
  30. package/dist/schemas/context-cache-warm.schema.json +222 -0
  31. package/dist/schemas/context-graph.schema.json +286 -0
  32. package/dist/schemas/context-pack.schema.json +246 -0
  33. package/dist/schemas/context-slice.schema.json +94 -0
  34. package/dist/schemas/context-source-manifest.schema.json +147 -0
  35. package/dist/schemas/schema-index.json +91 -0
  36. package/dist/schemas/session-start.schema.json +158 -0
  37. package/dist/schemas/task-close-repair-plan.schema.json +67 -0
  38. package/dist/schemas/task-context.schema.json +125 -0
  39. package/dist/schemas/task-finalize.schema.json +98 -0
  40. package/dist/schemas/task-lifecycle.schema.json +84 -0
  41. package/dist/services/capability-registry.js +266 -9
  42. package/dist/services/lifecycle-guide.js +23 -29
  43. package/dist/services/protocol-consistency.js +6 -8
  44. package/dist/services/workbench-next-actions.js +2 -0
  45. package/dist/task/acceptance.js +171 -0
  46. package/dist/task/task-close-repair-plan.js +190 -0
  47. package/dist/task/task-close.js +34 -35
  48. package/dist/task/task-finalize.js +377 -0
  49. package/dist/task/task-lifecycle.js +210 -0
  50. package/dist/task/task-next.js +10 -1
  51. package/dist/task/task-ready.js +4 -0
  52. package/package.json +1 -1
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CONTEXT_PACK_DEFAULT_BUDGET = exports.CONTEXT_PACK_COMMAND = exports.CONTEXT_PACK_SCHEMA_ID = void 0;
7
+ exports.buildContextPackReport = buildContextPackReport;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const context_graph_builder_1 = require("./context-graph-builder");
11
+ const context_slice_boundary_1 = require("./context-slice-boundary");
12
+ const extractor_contract_1 = require("./extractor-contract");
13
+ exports.CONTEXT_PACK_SCHEMA_ID = 'hadara.contextPack.v1';
14
+ exports.CONTEXT_PACK_COMMAND = 'context.pack';
15
+ exports.CONTEXT_PACK_DEFAULT_BUDGET = {
16
+ maxReadFirstItems: 7,
17
+ mode: 'bounded'
18
+ };
19
+ function buildContextPackReport(input) {
20
+ const generatedAt = input.generatedAt ?? new Date().toISOString();
21
+ const budget = normalizeContextBudget(input.budget);
22
+ const graphReport = input.graphReport ?? (0, context_graph_builder_1.buildContextGraphReport)({
23
+ projectRoot: input.projectRoot,
24
+ generatedAt,
25
+ includeCode: input.includeCode,
26
+ ...(input.taskId ? { taskId: input.taskId, mode: 'task' } : { mode: 'full' })
27
+ });
28
+ const taskId = input.taskId ?? graphReport.taskId ?? graphReport.stateProjection.summary.activeTask;
29
+ const issues = [];
30
+ const taskNode = taskId ? findTaskNode(graphReport.nodes, taskId) : undefined;
31
+ if (!taskId || !taskNode) {
32
+ issues.push({
33
+ severity: 'error',
34
+ code: 'CONTEXT_PACK_TASK_NOT_FOUND',
35
+ message: taskId
36
+ ? `Context pack could not find task ${taskId} in the context graph.`
37
+ : 'Context pack requires a task id or active task from state projection.',
38
+ fixHint: 'Pass --task <task-id> once the public context pack command exists, or repair project active task state.'
39
+ });
40
+ }
41
+ if (!graphReport.stateProjection) {
42
+ issues.push({
43
+ severity: 'warning',
44
+ code: 'CONTEXT_PACK_STATE_PROJECTION_UNAVAILABLE',
45
+ message: 'Context graph did not include a state projection.',
46
+ fixHint: 'Rebuild the context graph with state projection extraction enabled.'
47
+ });
48
+ }
49
+ if (graphReport.summary.degraded) {
50
+ issues.push({
51
+ severity: 'warning',
52
+ code: 'CONTEXT_PACK_DEGRADED',
53
+ message: 'Context graph is degraded; context pack output may be partial.'
54
+ });
55
+ }
56
+ if (input.includeCode && !codeIndexAvailable(graphReport)) {
57
+ issues.push({
58
+ severity: 'warning',
59
+ code: 'CONTEXT_PACK_CODE_INDEX_UNAVAILABLE',
60
+ message: 'Code-aware context pack was requested, but code index output is unavailable.',
61
+ fixHint: 'Build the graph with includeCode enabled after C2 code index support is available.'
62
+ });
63
+ }
64
+ const connectedIds = taskId ? connectedNodeIds(graphReport.edges, (0, extractor_contract_1.createTaskNodeId)(taskId)) : new Set();
65
+ const rankedNodes = rankContextPackNodes(graphReport.nodes, graphReport.edges, connectedIds, taskNode);
66
+ const readFirstRanked = rankedNodes.filter((ranked) => isReadFirstAllowed(ranked.node));
67
+ const readFirst = readFirstRanked
68
+ .slice(0, budget.maxReadFirstItems)
69
+ .map((ranked) => itemFromRankedNode(ranked, true, input.projectRoot));
70
+ if (readFirstRanked.length > readFirst.length) {
71
+ issues.push({
72
+ severity: 'warning',
73
+ code: 'CONTEXT_PACK_BUDGET_TRUNCATED',
74
+ message: `Context pack readFirst items were truncated from ${readFirstRanked.length} to ${readFirst.length}.`
75
+ });
76
+ }
77
+ const selectedIds = new Set(readFirst.map((item) => item.id));
78
+ const maxReadIfNeeded = Math.max(0, (budget.maxItems ?? 30) - readFirst.length);
79
+ const readIfNeededRanked = rankedNodes
80
+ .filter((ranked) => !selectedIds.has(ranked.node.id))
81
+ .filter((ranked) => !isDoNotReadByDefault(ranked.node));
82
+ const readIfNeeded = readIfNeededRanked
83
+ .slice(0, maxReadIfNeeded)
84
+ .map((ranked) => itemFromRankedNode(ranked, false, input.projectRoot));
85
+ if (readIfNeededRanked.length > readIfNeeded.length) {
86
+ issues.push({
87
+ severity: 'warning',
88
+ code: 'CONTEXT_PACK_BUDGET_TRUNCATED',
89
+ message: `Context pack readIfNeeded items were truncated from ${readIfNeededRanked.length} to ${readIfNeeded.length}.`
90
+ });
91
+ }
92
+ const doNotReadByDefault = graphReport.nodes
93
+ .filter((node) => node.type === 'Document')
94
+ .filter(isDoNotReadByDefault)
95
+ .map((node) => itemFromRankedNode({
96
+ node,
97
+ reason: 'Historical, archived, superseded, or excluded document is not read by default.',
98
+ confidence: 'derived',
99
+ score: 0
100
+ }, false, input.projectRoot))
101
+ .sort(compareItems);
102
+ const knownProblems = graphReport.nodes
103
+ .filter((node) => node.type === 'KnownProblem')
104
+ .map((node) => itemFromRankedNode({
105
+ node,
106
+ reason: 'Current project handoff records this known problem.',
107
+ confidence: 'explicit',
108
+ score: 0
109
+ }, false, input.projectRoot))
110
+ .sort(compareItems);
111
+ const cache = input.cache ?? graphReport.cache ?? { used: false, hit: false };
112
+ return {
113
+ schemaVersion: exports.CONTEXT_PACK_SCHEMA_ID,
114
+ command: exports.CONTEXT_PACK_COMMAND,
115
+ ok: issues.every((issue) => issue.severity !== 'error'),
116
+ generatedAt,
117
+ ...(taskId ? { taskId } : {}),
118
+ projectRoot: input.projectRoot,
119
+ budget,
120
+ readFirst,
121
+ readIfNeeded,
122
+ doNotReadByDefault,
123
+ validateWith: validationSuggestionsForTask(taskId, graphReport),
124
+ writeBoundaries: writeBoundariesForItems([...readFirst, ...readIfNeeded], graphReport.nodes),
125
+ sliceCandidates: sliceCandidatesForItems([...readFirst, ...readIfNeeded], graphReport.nodes),
126
+ knownProblems,
127
+ stateProjection: {
128
+ ...graphReport.stateProjection.summary,
129
+ issues: graphReport.stateProjection.issues
130
+ },
131
+ sourceSummary: {
132
+ graphAvailable: true,
133
+ codeIndexAvailable: codeIndexAvailable(graphReport),
134
+ stateProjectionAvailable: true,
135
+ docsRegistryAvailable: graphReport.stateProjection.sources.some((source) => source.kind === 'docs-registry'),
136
+ commandRegistryAvailable: graphReport.nodes.some((node) => node.type === 'Command'),
137
+ degraded: graphReport.summary.degraded || issues.some((issue) => issue.severity !== 'info'),
138
+ graphSourceHash: graphReport.sourceHash,
139
+ sourcesRead: graphReport.summary.sourcesRead
140
+ },
141
+ cache,
142
+ issues
143
+ };
144
+ }
145
+ function normalizeContextBudget(input = {}) {
146
+ const maxReadFirstItems = positiveInteger(input.maxReadFirstItems) ?? exports.CONTEXT_PACK_DEFAULT_BUDGET.maxReadFirstItems;
147
+ return {
148
+ ...(input.targetTokens !== undefined ? { targetTokens: input.targetTokens } : {}),
149
+ ...(input.maxItems !== undefined ? { maxItems: input.maxItems } : {}),
150
+ maxReadFirstItems,
151
+ mode: input.mode ?? exports.CONTEXT_PACK_DEFAULT_BUDGET.mode
152
+ };
153
+ }
154
+ function positiveInteger(value) {
155
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
156
+ }
157
+ function rankContextPackNodes(nodes, edges, connectedIds, taskNode) {
158
+ const ranked = new Map();
159
+ if (taskNode) {
160
+ ranked.set(taskNode.id, {
161
+ node: taskNode,
162
+ reason: 'Active task capsule is the first read for task-scoped context routing.',
163
+ confidence: 'explicit',
164
+ score: 1000
165
+ });
166
+ }
167
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
168
+ for (const edge of edges) {
169
+ if (!connectedIds.has(edge.from) && !connectedIds.has(edge.to))
170
+ continue;
171
+ for (const nodeId of [edge.from, edge.to]) {
172
+ const node = nodeById.get(nodeId);
173
+ if (!node || node.type === 'Task')
174
+ continue;
175
+ upsertRankedNode(ranked, {
176
+ node,
177
+ reason: edge.reason,
178
+ confidence: edge.confidence,
179
+ score: nodeScore(node, edge.confidence, connectedIds.has(node.id))
180
+ });
181
+ }
182
+ }
183
+ for (const node of nodes) {
184
+ if (node.type !== 'Document' || ranked.has(node.id))
185
+ continue;
186
+ if (metadataBoolean(node, 'requiredReading')) {
187
+ upsertRankedNode(ranked, {
188
+ node,
189
+ reason: 'Document registry marks this document as required reading.',
190
+ confidence: 'explicit',
191
+ score: nodeScore(node, 'explicit', false) - 25
192
+ });
193
+ }
194
+ }
195
+ return Array.from(ranked.values()).sort(compareRankedNodes);
196
+ }
197
+ function upsertRankedNode(ranked, candidate) {
198
+ const existing = ranked.get(candidate.node.id);
199
+ if (!existing || compareRankedNodes(candidate, existing) < 0) {
200
+ ranked.set(candidate.node.id, candidate);
201
+ }
202
+ }
203
+ function nodeScore(node, confidence, connected) {
204
+ const confidenceScore = confidence === 'explicit' ? 80 : confidence === 'derived' ? 50 : 20;
205
+ const connectedScore = connected ? 25 : 0;
206
+ const typeScore = {
207
+ Task: 1000,
208
+ Document: 700,
209
+ ManagedSection: 680,
210
+ Command: 620,
211
+ KnownProblem: 600,
212
+ Evidence: 550,
213
+ ReleaseCheck: 520,
214
+ Decision: 500,
215
+ SourceFile: 480,
216
+ TestFile: 460,
217
+ Symbol: 440,
218
+ FixtureFile: 380,
219
+ ConfigFile: 360
220
+ };
221
+ return typeScore[node.type] + confidenceScore + connectedScore;
222
+ }
223
+ function isReadFirstAllowed(node) {
224
+ if (isDoNotReadByDefault(node))
225
+ return false;
226
+ return ['Task', 'Document', 'ManagedSection', 'Command', 'SourceFile', 'TestFile', 'Symbol', 'KnownProblem'].includes(node.type);
227
+ }
228
+ function isDoNotReadByDefault(node) {
229
+ const status = String(node.status ?? '').toLowerCase();
230
+ const kind = String(node.kind ?? '').toLowerCase();
231
+ const readWhen = Array.isArray(node.metadata?.readWhen) ? node.metadata.readWhen.map(String) : [];
232
+ return status === 'archived'
233
+ || status === 'superseded'
234
+ || kind.includes('historical')
235
+ || readWhen.includes('historical')
236
+ || readWhen.includes('never-default')
237
+ || (metadataBoolean(node, 'requiredReading') === false && kind.includes('archive'));
238
+ }
239
+ function itemFromRankedNode(ranked, required, projectRoot) {
240
+ const line = ranked.node.source.line;
241
+ const sourceHash = sourceHashForItem(projectRoot, ranked.node);
242
+ return {
243
+ id: ranked.node.id,
244
+ type: ranked.node.type,
245
+ ...(ranked.node.path ? { path: ranked.node.path } : {}),
246
+ ...(line ? { lineStart: line, lineEnd: line } : {}),
247
+ title: ranked.node.label,
248
+ reason: ranked.reason,
249
+ confidence: ranked.confidence,
250
+ ...(sourceHash ? { sourceHash } : {}),
251
+ ...(ranked.node.path ? { estimatedTokens: estimateTokens(ranked.node) } : {}),
252
+ required,
253
+ sourceAccess: sourceAccessForNode(ranked.node)
254
+ };
255
+ }
256
+ function sourceHashForItem(projectRoot, node) {
257
+ if (!node.path || !(0, context_slice_boundary_1.isContextSliceProjectRelativePath)(node.path))
258
+ return node.source.hash;
259
+ const normalized = (0, context_slice_boundary_1.normalizeContextSliceInputPath)(node.path);
260
+ const root = node_path_1.default.resolve(projectRoot);
261
+ const absolutePath = node_path_1.default.resolve(root, normalized);
262
+ if (absolutePath !== root && !absolutePath.startsWith(`${root}${node_path_1.default.sep}`))
263
+ return node.source.hash;
264
+ try {
265
+ const stat = node_fs_1.default.statSync(absolutePath);
266
+ if (!stat.isFile())
267
+ return node.source.hash;
268
+ return (0, extractor_contract_1.hashContextGraphText)(node_fs_1.default.readFileSync(absolutePath, 'utf8'));
269
+ }
270
+ catch {
271
+ return node.source.hash;
272
+ }
273
+ }
274
+ function sourceAccessForNode(node) {
275
+ if (!node.path) {
276
+ return {
277
+ rawSlice: 'not-applicable',
278
+ reason: 'This context item has no project file path for raw context slicing.'
279
+ };
280
+ }
281
+ if ((0, context_slice_boundary_1.isContextSliceProjectRelativePath)(node.path)) {
282
+ return {
283
+ rawSlice: 'sliceable',
284
+ reason: 'This item path is inside the raw context-slice read boundary.'
285
+ };
286
+ }
287
+ return {
288
+ rawSlice: 'not-sliceable',
289
+ reason: 'This item remains graph context, but its path is outside the raw context-slice read boundary.'
290
+ };
291
+ }
292
+ function estimateTokens(node) {
293
+ const labelCost = Math.ceil(node.label.length / 4);
294
+ const pathCost = node.path ? Math.ceil(node.path.length / 4) : 0;
295
+ return Math.max(24, labelCost + pathCost + 16);
296
+ }
297
+ function validationSuggestionsForTask(taskId, graphReport) {
298
+ const suggestions = new Map();
299
+ if (taskId) {
300
+ suggestions.set(`ready:${taskId}`, {
301
+ command: `node dist/cli/main.js task ready --task ${taskId} --level done --json`,
302
+ reason: 'Done-level readiness is required before closing this task.',
303
+ requiredForClose: true,
304
+ source: 'evidence-history'
305
+ });
306
+ }
307
+ for (const suggestion of graphReport.taskContext?.validationSuggestions ?? []) {
308
+ const source = suggestion.includes('npm run') || suggestion.includes('test')
309
+ ? 'task-tests'
310
+ : suggestion.includes('release')
311
+ ? 'release-readiness'
312
+ : 'heuristic';
313
+ suggestions.set(suggestion, {
314
+ command: suggestion,
315
+ reason: 'Task context report suggested this validation command.',
316
+ requiredForClose: false,
317
+ source
318
+ });
319
+ }
320
+ return Array.from(suggestions.values()).sort((a, b) => a.command.localeCompare(b.command));
321
+ }
322
+ function writeBoundariesForItems(items, nodes) {
323
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
324
+ const boundaries = new Map();
325
+ for (const item of items) {
326
+ if (!item.path)
327
+ continue;
328
+ const node = nodeById.get(item.id);
329
+ const boundary = boundaryForPath(item.path, node);
330
+ boundaries.set(item.path, {
331
+ path: item.path,
332
+ boundary,
333
+ reason: boundaryReason(item.path, boundary)
334
+ });
335
+ }
336
+ return Array.from(boundaries.values()).sort((a, b) => a.path.localeCompare(b.path));
337
+ }
338
+ function boundaryForPath(path, node) {
339
+ if (path.endsWith('evidence.jsonl'))
340
+ return 'append-only';
341
+ if (path === 'docs/TASK_BOARD.md' || path.endsWith('/TASK.md'))
342
+ return 'dry-run-first';
343
+ if (node?.type === 'ManagedSection')
344
+ return 'managed-section';
345
+ if (path.startsWith('docs/') || path.startsWith('tasks/'))
346
+ return 'agent-freeform';
347
+ return 'read-only';
348
+ }
349
+ function boundaryReason(path, boundary) {
350
+ if (boundary === 'append-only')
351
+ return 'Evidence files are append-only and must use the evidence writer.';
352
+ if (boundary === 'dry-run-first')
353
+ return `${path} has command-owned lifecycle fields; use dry-run-first lifecycle commands where applicable.`;
354
+ if (boundary === 'managed-section')
355
+ return 'Managed section edits must preserve HADARA markers.';
356
+ if (boundary === 'agent-freeform')
357
+ return 'Project documentation or task capsule prose can be edited by the agent within task scope.';
358
+ return 'Read for context only; do not mutate from context routing commands.';
359
+ }
360
+ function sliceCandidatesForItems(items, nodes) {
361
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
362
+ return items
363
+ .filter((item) => item.path)
364
+ .filter((item) => (0, context_slice_boundary_1.isContextSliceProjectRelativePath)(item.path))
365
+ .filter((item) => ['Document', 'ManagedSection', 'SourceFile', 'TestFile', 'Symbol'].includes(item.type))
366
+ .slice(0, 10)
367
+ .map((item, index) => {
368
+ const node = nodeById.get(item.id);
369
+ const strategy = sliceStrategyForNode(node);
370
+ const path = item.path;
371
+ const range = explicitRangeForCandidate(item, node);
372
+ const lineStart = range?.lineStart;
373
+ const lineEnd = range?.lineEnd;
374
+ const symbol = node?.type === 'Symbol' ? node.label : undefined;
375
+ const managedSection = node?.type === 'ManagedSection' ? node.label : undefined;
376
+ const suggestedCommandArgs = suggestedSliceCommandArgs(path, strategy, node, { lineStart, lineEnd, symbol, managedSection });
377
+ return {
378
+ id: `slice-candidate:${index + 1}:${item.id}`,
379
+ path,
380
+ strategy,
381
+ ...(lineStart ? { lineStart } : {}),
382
+ ...(lineEnd ? { lineEnd } : {}),
383
+ ...(symbol ? { symbol } : {}),
384
+ ...(managedSection ? { managedSection } : {}),
385
+ reason: `Candidate slice for ${item.id} selected by context pack ranking.`,
386
+ suggestedCommand: suggestedSliceCommand(suggestedCommandArgs),
387
+ suggestedCommandArgs
388
+ };
389
+ });
390
+ }
391
+ function explicitRangeForCandidate(item, node) {
392
+ const lineStart = item.lineStart ?? numberMetadata(node, 'startLine') ?? node?.source.line ?? 1;
393
+ const metadataEnd = numberMetadata(node, 'endLine');
394
+ if (metadataEnd !== undefined && metadataEnd > lineStart)
395
+ return { lineStart, lineEnd: metadataEnd };
396
+ if (item.lineEnd !== undefined && item.lineEnd > lineStart)
397
+ return { lineStart, lineEnd: item.lineEnd };
398
+ return { lineStart, lineEnd: Math.max(lineStart, lineStart + 80) };
399
+ }
400
+ function sliceStrategyForNode(node) {
401
+ if (node?.type === 'ManagedSection')
402
+ return 'managed-section';
403
+ if (node?.type === 'Symbol')
404
+ return 'symbol-neighborhood';
405
+ return 'explicit-range';
406
+ }
407
+ function suggestedSliceCommandArgs(path, strategy, node, hints = {}) {
408
+ const base = ['context', 'slice', '--path', path];
409
+ if (strategy === 'managed-section')
410
+ return [...base, '--managed-section', hints.managedSection ?? node?.label ?? '<section-id>', '--json'];
411
+ if (strategy === 'symbol-neighborhood')
412
+ return [...base, '--symbol', hints.symbol ?? node?.label ?? '<symbol>', '--json'];
413
+ const startLine = hints.lineStart ?? node?.source.line ?? 1;
414
+ const endLine = hints.lineEnd ?? Math.max(startLine, startLine + 80);
415
+ return [...base, '--from', String(startLine), '--to', String(endLine), '--json'];
416
+ }
417
+ function suggestedSliceCommand(args) {
418
+ return ['hadara', ...args].map(shellQuote).join(' ');
419
+ }
420
+ function shellQuote(value) {
421
+ if (/^[A-Za-z0-9_./:@+-]+$/.test(value))
422
+ return value;
423
+ return `'${value.replace(/'/g, "'\\''")}'`;
424
+ }
425
+ function numberMetadata(node, key) {
426
+ const value = node?.metadata?.[key];
427
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
428
+ }
429
+ function findTaskNode(nodes, taskId) {
430
+ const nodeId = (0, extractor_contract_1.createTaskNodeId)(taskId);
431
+ const matches = nodes.filter((node) => node.id === nodeId && node.type === 'Task');
432
+ return matches.find((node) => node.kind === 'task-capsule') ?? matches[0];
433
+ }
434
+ function connectedNodeIds(edges, taskNodeId) {
435
+ const ids = new Set([taskNodeId]);
436
+ for (const edge of edges) {
437
+ if (edge.from === taskNodeId)
438
+ ids.add(edge.to);
439
+ if (edge.to === taskNodeId)
440
+ ids.add(edge.from);
441
+ }
442
+ return ids;
443
+ }
444
+ function codeIndexAvailable(graphReport) {
445
+ return graphReport.stateProjection.sources.some((source) => source.kind === 'code-index')
446
+ || graphReport.nodes.some((node) => ['SourceFile', 'TestFile', 'Symbol'].includes(node.type));
447
+ }
448
+ function metadataBoolean(node, key) {
449
+ const value = node.metadata?.[key];
450
+ return typeof value === 'boolean' ? value : undefined;
451
+ }
452
+ function compareRankedNodes(a, b) {
453
+ return b.score - a.score || a.node.id.localeCompare(b.node.id);
454
+ }
455
+ function compareItems(a, b) {
456
+ return a.id.localeCompare(b.id);
457
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CONTEXT_SLICE_DENIED_PATHS = exports.CONTEXT_SLICE_HADARA_ALLOWLIST = void 0;
7
+ exports.normalizeContextSliceInputPath = normalizeContextSliceInputPath;
8
+ exports.isDeniedContextSlicePath = isDeniedContextSlicePath;
9
+ exports.isContextSliceProjectRelativePath = isContextSliceProjectRelativePath;
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const extractor_contract_1 = require("./extractor-contract");
12
+ exports.CONTEXT_SLICE_HADARA_ALLOWLIST = new Set([
13
+ '.hadara/context/HADARA_CONTEXT.md',
14
+ '.hadara/docs-registry.json'
15
+ ]);
16
+ exports.CONTEXT_SLICE_DENIED_PATHS = [
17
+ '.git',
18
+ 'node_modules',
19
+ '.hadara/tmp',
20
+ '.hadara/run',
21
+ '.dashboard-visual'
22
+ ];
23
+ function normalizeContextSliceInputPath(inputPath) {
24
+ return inputPath ? (0, extractor_contract_1.normalizeContextGraphPath)(inputPath.replace(/^\.?\//, '')) : '';
25
+ }
26
+ function isDeniedContextSlicePath(normalized) {
27
+ if (normalized.startsWith('.hadara/') && !exports.CONTEXT_SLICE_HADARA_ALLOWLIST.has(normalized))
28
+ return true;
29
+ return exports.CONTEXT_SLICE_DENIED_PATHS.some((denied) => normalized === denied || normalized.startsWith(`${denied}/`));
30
+ }
31
+ function isContextSliceProjectRelativePath(inputPath) {
32
+ const normalized = normalizeContextSliceInputPath(inputPath);
33
+ return Boolean(normalized)
34
+ && !node_path_1.default.isAbsolute(inputPath ?? '')
35
+ && !normalized.split('/').includes('..')
36
+ && !isDeniedContextSlicePath(normalized);
37
+ }