hadara 0.3.2 → 0.3.3-rc.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/README.md +38 -23
- package/dist/cli/context.js +110 -0
- package/dist/cli/help.js +6 -7
- package/dist/cli/init.js +56 -58
- package/dist/cli/main.js +12 -0
- package/dist/cli/session.js +37 -0
- package/dist/cli/task.js +52 -0
- package/dist/context/code-graph-extractor.js +123 -0
- package/dist/context/code-index.js +1154 -0
- package/dist/context/context-cache-store.js +925 -0
- package/dist/context/context-graph-builder.js +341 -0
- package/dist/context/context-graph.js +42 -0
- package/dist/context/context-pack.js +457 -0
- package/dist/context/context-slice-boundary.js +37 -0
- package/dist/context/context-slice.js +487 -0
- package/dist/context/document-extractors.js +343 -0
- package/dist/context/evidence-extractors.js +179 -0
- package/dist/context/extractor-contract.js +166 -0
- package/dist/context/registry-extractors.js +177 -0
- package/dist/context/release-extractors.js +175 -0
- package/dist/context/session-start.js +297 -0
- package/dist/context/source-manifest.js +566 -0
- package/dist/context/state-projection.js +196 -0
- package/dist/context/task-extractors.js +168 -0
- package/dist/core/schema.js +26 -0
- package/dist/harness/validate.js +7 -8
- package/dist/schemas/code-index.schema.json +173 -0
- package/dist/schemas/context-cache-record.schema.json +56 -0
- package/dist/schemas/context-cache-status.schema.json +167 -0
- package/dist/schemas/context-cache-warm.schema.json +222 -0
- package/dist/schemas/context-graph.schema.json +286 -0
- package/dist/schemas/context-pack.schema.json +246 -0
- package/dist/schemas/context-slice.schema.json +94 -0
- package/dist/schemas/context-source-manifest.schema.json +147 -0
- package/dist/schemas/schema-index.json +91 -0
- package/dist/schemas/session-start.schema.json +158 -0
- package/dist/schemas/task-close-repair-plan.schema.json +67 -0
- package/dist/schemas/task-context.schema.json +125 -0
- package/dist/schemas/task-finalize.schema.json +98 -0
- package/dist/schemas/task-lifecycle.schema.json +84 -0
- package/dist/services/capability-registry.js +266 -9
- package/dist/services/lifecycle-guide.js +23 -29
- package/dist/services/protocol-consistency.js +6 -8
- package/dist/task/acceptance.js +171 -0
- package/dist/task/task-close-repair-plan.js +190 -0
- package/dist/task/task-close.js +34 -35
- package/dist/task/task-finalize.js +377 -0
- package/dist/task/task-lifecycle.js +210 -0
- package/dist/task/task-next.js +10 -1
- package/dist/task/task-ready.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,343 @@
|
|
|
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.extractManagedSections = extractManagedSections;
|
|
7
|
+
exports.extractProjectState = extractProjectState;
|
|
8
|
+
exports.extractDecisions = extractDecisions;
|
|
9
|
+
exports.extractAgentHandoff = extractAgentHandoff;
|
|
10
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const extractor_contract_1 = require("./extractor-contract");
|
|
13
|
+
const managed_sections_1 = require("../services/managed-sections");
|
|
14
|
+
const markdown_table_1 = require("../services/markdown-table");
|
|
15
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
16
|
+
const PROJECT_MANAGED_TARGETS = [
|
|
17
|
+
'docs/TASK_BOARD.md',
|
|
18
|
+
'docs/PROJECT_STATE.md',
|
|
19
|
+
'docs/AGENT_HANDOFF.md',
|
|
20
|
+
'docs/IMPLEMENTATION_SOP.md',
|
|
21
|
+
'docs/DOC_REGISTRY.md'
|
|
22
|
+
];
|
|
23
|
+
function extractManagedSections(projectRoot) {
|
|
24
|
+
const targets = listManagedSectionTargets(projectRoot);
|
|
25
|
+
const sources = targets.map((target) => readSource(projectRoot, target));
|
|
26
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractManagedSections', sources);
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
const content = readOptionalText(node_path_1.default.join(projectRoot, target));
|
|
29
|
+
if (content == null)
|
|
30
|
+
continue;
|
|
31
|
+
const sourceHash = (0, extractor_contract_1.hashContextGraphText)(content);
|
|
32
|
+
const parsed = (0, managed_sections_1.parseManagedSections)(content, target);
|
|
33
|
+
result.issues.push(...parsed.issues.map((issue) => managedSectionIssue(issue)));
|
|
34
|
+
for (const section of parsed.sections) {
|
|
35
|
+
const source = (0, extractor_contract_1.createContextGraphSourceRef)({
|
|
36
|
+
path: target,
|
|
37
|
+
extractor: 'extractManagedSections',
|
|
38
|
+
line: section.startLine,
|
|
39
|
+
hash: sourceHash
|
|
40
|
+
});
|
|
41
|
+
result.nodes.push(managedSectionNode(section, source));
|
|
42
|
+
result.edges.push(edge('BELONGS_TO_DOCUMENT', (0, extractor_contract_1.createManagedSectionNodeId)(section.path, section.id), (0, extractor_contract_1.createDocumentNodeId)(section.path), source, `${section.id} belongs to ${section.path}.`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function extractProjectState(projectRoot) {
|
|
48
|
+
const relativePath = 'docs/PROJECT_STATE.md';
|
|
49
|
+
const content = readOptionalText(node_path_1.default.join(projectRoot, relativePath));
|
|
50
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractProjectState', [{ path: relativePath, content }]);
|
|
51
|
+
if (content == null) {
|
|
52
|
+
result.issues.push({
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
code: 'CONTEXT_GRAPH_SOURCE_MISSING',
|
|
55
|
+
path: relativePath,
|
|
56
|
+
message: 'docs/PROJECT_STATE.md is missing; project-state hints cannot be extracted.',
|
|
57
|
+
fixHint: 'Restore docs/PROJECT_STATE.md before relying on state projection context routing.'
|
|
58
|
+
});
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
const sourceHash = (0, extractor_contract_1.hashContextGraphText)(content);
|
|
62
|
+
result.stateSources?.push(projectStateSource(relativePath, sourceHash, extractProjectStateHints(content)));
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
function extractDecisions(projectRoot) {
|
|
66
|
+
const targets = listDecisionTargets(projectRoot);
|
|
67
|
+
const sources = targets.map((target) => readSource(projectRoot, target.path));
|
|
68
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractDecisions', sources);
|
|
69
|
+
for (const target of targets) {
|
|
70
|
+
const content = readOptionalText(node_path_1.default.join(projectRoot, target.path));
|
|
71
|
+
if (content == null)
|
|
72
|
+
continue;
|
|
73
|
+
const sourceHash = (0, extractor_contract_1.hashContextGraphText)(content);
|
|
74
|
+
for (const decision of parseDecisionRecords(content)) {
|
|
75
|
+
const source = (0, extractor_contract_1.createContextGraphSourceRef)({
|
|
76
|
+
path: target.path,
|
|
77
|
+
extractor: 'extractDecisions',
|
|
78
|
+
line: decision.line,
|
|
79
|
+
hash: sourceHash
|
|
80
|
+
});
|
|
81
|
+
const decisionNodeId = (0, extractor_contract_1.createDecisionNodeId)(target.path, decision.id);
|
|
82
|
+
result.nodes.push(decisionNode(target.path, decision, source));
|
|
83
|
+
result.edges.push(edge('BELONGS_TO_DOCUMENT', decisionNodeId, (0, extractor_contract_1.createDocumentNodeId)(target.path), source, `${decision.id} belongs to ${target.path}.`));
|
|
84
|
+
result.edges.push(edge('HAS_DECISION', decisionOwnerNodeId(target), decisionNodeId, source, `${decisionOwnerNodeId(target)} has decision ${decision.id}.`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function extractAgentHandoff(projectRoot) {
|
|
90
|
+
const relativePath = 'docs/AGENT_HANDOFF.md';
|
|
91
|
+
const content = readOptionalText(node_path_1.default.join(projectRoot, relativePath));
|
|
92
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractAgentHandoff', [{ path: relativePath, content }]);
|
|
93
|
+
if (content == null) {
|
|
94
|
+
result.issues.push({
|
|
95
|
+
severity: 'warning',
|
|
96
|
+
code: 'CONTEXT_GRAPH_SOURCE_MISSING',
|
|
97
|
+
path: relativePath,
|
|
98
|
+
message: 'docs/AGENT_HANDOFF.md is missing; KnownProblem nodes cannot be extracted from current handoff state.',
|
|
99
|
+
fixHint: 'Restore docs/AGENT_HANDOFF.md before relying on known-problem context routing.'
|
|
100
|
+
});
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
const sourceHash = (0, extractor_contract_1.hashContextGraphText)(content);
|
|
104
|
+
const knownProblems = parseKnownProblems(content);
|
|
105
|
+
result.stateSources?.push(agentHandoffStateSource(relativePath, sourceHash, extractAgentHandoffHints(content), knownProblems.length));
|
|
106
|
+
for (const problem of knownProblems) {
|
|
107
|
+
const source = (0, extractor_contract_1.createContextGraphSourceRef)({
|
|
108
|
+
path: relativePath,
|
|
109
|
+
extractor: 'extractAgentHandoff',
|
|
110
|
+
line: problem.line,
|
|
111
|
+
hash: sourceHash
|
|
112
|
+
});
|
|
113
|
+
const problemNodeId = (0, extractor_contract_1.createKnownProblemNodeId)(relativePath, problem.issue);
|
|
114
|
+
result.nodes.push(knownProblemNode(relativePath, problem, source));
|
|
115
|
+
result.edges.push(edge('HAS_KNOWN_PROBLEM', (0, extractor_contract_1.createDocumentNodeId)(relativePath), problemNodeId, source, `docs/AGENT_HANDOFF.md records known problem: ${problem.issue}`));
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function projectStateSource(relativePath, sourceHash, hints) {
|
|
120
|
+
return {
|
|
121
|
+
id: 'state-source:project-state',
|
|
122
|
+
path: (0, extractor_contract_1.normalizeContextGraphPath)(relativePath),
|
|
123
|
+
kind: 'project-state',
|
|
124
|
+
hash: sourceHash,
|
|
125
|
+
extracted: hints
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function agentHandoffStateSource(relativePath, sourceHash, hints, knownProblems) {
|
|
129
|
+
return {
|
|
130
|
+
id: 'state-source:agent-handoff',
|
|
131
|
+
path: (0, extractor_contract_1.normalizeContextGraphPath)(relativePath),
|
|
132
|
+
kind: 'agent-handoff',
|
|
133
|
+
hash: sourceHash,
|
|
134
|
+
extracted: {
|
|
135
|
+
...hints,
|
|
136
|
+
knownProblems
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function extractProjectStateHints(content) {
|
|
141
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(content, '## Metadata');
|
|
142
|
+
const latestRaw = (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Latest Completed Task')?.[1] ?? findLineValue(content, /latest completed task is\s+([^\n.]+)/i);
|
|
143
|
+
const activeRaw = (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Active Task')?.[1] ?? null;
|
|
144
|
+
return {
|
|
145
|
+
latestCompletedTask: normalizeTaskHint(latestRaw),
|
|
146
|
+
activeTask: normalizeTaskHint(activeRaw),
|
|
147
|
+
latestCompletedRaw: latestRaw ?? null,
|
|
148
|
+
activeRaw: activeRaw ?? null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function extractAgentHandoffHints(content) {
|
|
152
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(content, '## Current State');
|
|
153
|
+
const latestRaw = (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Latest Completed Task')?.[1] ?? null;
|
|
154
|
+
const activeRaw = (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Active / Next Task')?.[1] ?? (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Active Task')?.[1] ?? null;
|
|
155
|
+
return {
|
|
156
|
+
latestCompletedTask: normalizeTaskHint(latestRaw),
|
|
157
|
+
activeTask: normalizeTaskHint(activeRaw),
|
|
158
|
+
latestCompletedRaw: latestRaw,
|
|
159
|
+
activeRaw: activeRaw
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function normalizeTaskHint(value) {
|
|
163
|
+
if (!value)
|
|
164
|
+
return null;
|
|
165
|
+
if (/^(none|n\/a|tbd)\b/i.test(value.trim()))
|
|
166
|
+
return null;
|
|
167
|
+
return value.match(/\bT-\d{4}\b/)?.[0] ?? null;
|
|
168
|
+
}
|
|
169
|
+
function findLineValue(content, pattern) {
|
|
170
|
+
for (const line of content.split(/\r?\n/)) {
|
|
171
|
+
const match = line.match(pattern);
|
|
172
|
+
if (match?.[1])
|
|
173
|
+
return match[1].trim();
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function managedSectionNode(section, source) {
|
|
178
|
+
return {
|
|
179
|
+
id: (0, extractor_contract_1.createManagedSectionNodeId)(section.path, section.id),
|
|
180
|
+
type: 'ManagedSection',
|
|
181
|
+
label: section.id,
|
|
182
|
+
path: (0, extractor_contract_1.normalizeContextGraphPath)(section.path),
|
|
183
|
+
kind: section.metadata.kind,
|
|
184
|
+
owner: section.metadata.owner,
|
|
185
|
+
metadata: {
|
|
186
|
+
mode: section.metadata.mode,
|
|
187
|
+
version: section.metadata.version,
|
|
188
|
+
required: section.metadata.required ?? false,
|
|
189
|
+
closeSourceRole: section.metadata.closeSourceRole ?? null,
|
|
190
|
+
startLine: section.startLine,
|
|
191
|
+
endLine: section.endLine,
|
|
192
|
+
sectionBeforeHash: section.sectionBeforeHash
|
|
193
|
+
},
|
|
194
|
+
source
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function decisionNode(documentPath, decision, source) {
|
|
198
|
+
return {
|
|
199
|
+
id: (0, extractor_contract_1.createDecisionNodeId)(documentPath, decision.id),
|
|
200
|
+
type: 'Decision',
|
|
201
|
+
label: `${decision.id} ${decision.decision}`.trim(),
|
|
202
|
+
path: (0, extractor_contract_1.normalizeContextGraphPath)(documentPath),
|
|
203
|
+
status: decision.status,
|
|
204
|
+
kind: 'decision-record',
|
|
205
|
+
metadata: {
|
|
206
|
+
decision: decision.decision,
|
|
207
|
+
rationale: decision.rationale ?? null,
|
|
208
|
+
evidence: decision.evidence ?? null
|
|
209
|
+
},
|
|
210
|
+
source
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function knownProblemNode(documentPath, problem, source) {
|
|
214
|
+
return {
|
|
215
|
+
id: (0, extractor_contract_1.createKnownProblemNodeId)(documentPath, problem.issue),
|
|
216
|
+
type: 'KnownProblem',
|
|
217
|
+
label: problem.issue,
|
|
218
|
+
path: (0, extractor_contract_1.normalizeContextGraphPath)(documentPath),
|
|
219
|
+
kind: 'current-known-problem',
|
|
220
|
+
metadata: {
|
|
221
|
+
impact: problem.impact ?? null,
|
|
222
|
+
nextStep: problem.nextStep ?? null
|
|
223
|
+
},
|
|
224
|
+
source
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function parseDecisionRecords(content) {
|
|
228
|
+
const rows = parseDecisionTableRows(content);
|
|
229
|
+
if (rows.length > 0)
|
|
230
|
+
return rows;
|
|
231
|
+
return parseDecisionHeadingRecords(content);
|
|
232
|
+
}
|
|
233
|
+
function parseDecisionTableRows(content) {
|
|
234
|
+
const decisions = [];
|
|
235
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
236
|
+
const trimmed = line.trim();
|
|
237
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|') || /^\|\s*:?-+/.test(trimmed))
|
|
238
|
+
return;
|
|
239
|
+
const cells = trimmed.slice(1, -1).split('|').map((cell) => cell.trim());
|
|
240
|
+
if (!/^D-\d+/i.test(cells[0] ?? ''))
|
|
241
|
+
return;
|
|
242
|
+
const hasDateColumn = /^\d{4}-\d{2}-\d{2}$/.test(cells[1] ?? '') || cells.length >= 6;
|
|
243
|
+
decisions.push({
|
|
244
|
+
id: cells[0],
|
|
245
|
+
decision: cells[hasDateColumn ? 2 : 1] ?? '',
|
|
246
|
+
status: cells[hasDateColumn ? 3 : 2] ?? 'recorded',
|
|
247
|
+
rationale: cells[hasDateColumn ? 4 : 3],
|
|
248
|
+
evidence: cells[hasDateColumn ? 5 : 4],
|
|
249
|
+
line: index + 1
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
return decisions;
|
|
253
|
+
}
|
|
254
|
+
function parseDecisionHeadingRecords(content) {
|
|
255
|
+
const lines = content.split(/\r?\n/);
|
|
256
|
+
const records = [];
|
|
257
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
258
|
+
const match = lines[index].match(/^##\s+(D-\d+):?\s*(.*)$/i);
|
|
259
|
+
if (!match)
|
|
260
|
+
continue;
|
|
261
|
+
const body = [];
|
|
262
|
+
for (let next = index + 1; next < lines.length && !/^##\s+/.test(lines[next]); next += 1) {
|
|
263
|
+
if (lines[next].trim())
|
|
264
|
+
body.push(lines[next].trim());
|
|
265
|
+
}
|
|
266
|
+
records.push({
|
|
267
|
+
id: match[1],
|
|
268
|
+
decision: match[2]?.trim() || match[1],
|
|
269
|
+
status: 'recorded',
|
|
270
|
+
rationale: body.join('\n') || undefined,
|
|
271
|
+
line: index + 1
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return records;
|
|
275
|
+
}
|
|
276
|
+
function parseKnownProblems(content) {
|
|
277
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(content, '## Current Known Problems');
|
|
278
|
+
return rows
|
|
279
|
+
.filter((row) => row[0] && row[0] !== 'Issue')
|
|
280
|
+
.map((row) => ({
|
|
281
|
+
issue: row[0],
|
|
282
|
+
impact: row[1],
|
|
283
|
+
nextStep: row[2],
|
|
284
|
+
line: findLineContaining(content, row[0])
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
function listManagedSectionTargets(projectRoot) {
|
|
288
|
+
const taskTargets = (0, task_capsule_1.listTaskCapsules)(projectRoot).flatMap((task) => [
|
|
289
|
+
(0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, node_path_1.default.join(task.dir, 'TASK.md')),
|
|
290
|
+
(0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, node_path_1.default.join(task.dir, 'HANDOFF.md'))
|
|
291
|
+
]);
|
|
292
|
+
return uniqueSorted(PROJECT_MANAGED_TARGETS.concat(taskTargets)).filter((target) => node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, target)));
|
|
293
|
+
}
|
|
294
|
+
function listDecisionTargets(projectRoot) {
|
|
295
|
+
const targets = [];
|
|
296
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, 'docs/DECISIONS.md')))
|
|
297
|
+
targets.push({ path: 'docs/DECISIONS.md' });
|
|
298
|
+
for (const task of (0, task_capsule_1.listTaskCapsules)(projectRoot)) {
|
|
299
|
+
const decisionPath = (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, node_path_1.default.join(task.dir, 'DECISIONS.md'));
|
|
300
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, decisionPath)))
|
|
301
|
+
targets.push({ path: decisionPath, task });
|
|
302
|
+
}
|
|
303
|
+
return targets;
|
|
304
|
+
}
|
|
305
|
+
function decisionOwnerNodeId(target) {
|
|
306
|
+
return target.task ? (0, extractor_contract_1.createTaskNodeId)(target.task.id) : (0, extractor_contract_1.createDocumentNodeId)(target.path);
|
|
307
|
+
}
|
|
308
|
+
function edge(type, from, to, source, reason) {
|
|
309
|
+
return {
|
|
310
|
+
id: (0, extractor_contract_1.createContextGraphEdgeId)({ type, from, to, source, reason }),
|
|
311
|
+
from,
|
|
312
|
+
to,
|
|
313
|
+
type,
|
|
314
|
+
confidence: 'explicit',
|
|
315
|
+
reason,
|
|
316
|
+
source
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function readSource(projectRoot, relativePath) {
|
|
320
|
+
return {
|
|
321
|
+
path: relativePath,
|
|
322
|
+
content: readOptionalText(node_path_1.default.join(projectRoot, relativePath))
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function readOptionalText(absolutePath) {
|
|
326
|
+
return node_fs_1.default.existsSync(absolutePath) ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : null;
|
|
327
|
+
}
|
|
328
|
+
function managedSectionIssue(issue) {
|
|
329
|
+
return {
|
|
330
|
+
severity: issue.severity === 'error' ? 'warning' : issue.severity,
|
|
331
|
+
code: 'CONTEXT_GRAPH_PARSE_FAILED',
|
|
332
|
+
path: issue.path,
|
|
333
|
+
message: issue.message,
|
|
334
|
+
fixHint: issue.sectionId ? `Repair managed section ${issue.sectionId} in ${issue.path}.` : `Repair managed section markers in ${issue.path}.`
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function findLineContaining(content, needle) {
|
|
338
|
+
const index = content.split(/\r?\n/).findIndex((line) => line.includes(needle));
|
|
339
|
+
return index >= 0 ? index + 1 : undefined;
|
|
340
|
+
}
|
|
341
|
+
function uniqueSorted(values) {
|
|
342
|
+
return Array.from(new Set(values.map(extractor_contract_1.normalizeContextGraphPath))).sort();
|
|
343
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
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.extractEvidence = extractEvidence;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const extractor_contract_1 = require("./extractor-contract");
|
|
10
|
+
const normalizer_1 = require("../evidence/normalizer");
|
|
11
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
12
|
+
function extractEvidence(projectRoot) {
|
|
13
|
+
const capsules = (0, task_capsule_1.listTaskCapsules)(projectRoot);
|
|
14
|
+
const sources = capsules.map((task) => readEvidenceSource(projectRoot, task));
|
|
15
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractEvidence', sources);
|
|
16
|
+
for (const task of capsules) {
|
|
17
|
+
const evidencePath = node_path_1.default.join(task.dir, 'evidence.jsonl');
|
|
18
|
+
const relativePath = (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, evidencePath);
|
|
19
|
+
const content = readOptionalText(evidencePath);
|
|
20
|
+
if (content == null) {
|
|
21
|
+
result.issues.push(evidenceReadFailedIssue(relativePath, `Task Capsule ${task.id} is missing evidence.jsonl.`));
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const sourceHash = (0, extractor_contract_1.hashContextGraphText)(content);
|
|
25
|
+
const normalizedRecords = parseEvidenceJsonl(content, relativePath, result.issues)
|
|
26
|
+
.map((entry) => normalizeEntry(entry, task, relativePath, result.issues))
|
|
27
|
+
.filter((entry) => Boolean(entry));
|
|
28
|
+
for (const record of normalizedRecords) {
|
|
29
|
+
const source = (0, extractor_contract_1.createContextGraphSourceRef)({
|
|
30
|
+
path: relativePath,
|
|
31
|
+
extractor: 'extractEvidence',
|
|
32
|
+
line: record.sourceLine,
|
|
33
|
+
hash: sourceHash
|
|
34
|
+
});
|
|
35
|
+
result.nodes.push(evidenceNode(record, source));
|
|
36
|
+
result.edges.push(...evidenceEdges(task, record, source));
|
|
37
|
+
}
|
|
38
|
+
result.stateSources?.push(evidenceStateSource(task, relativePath, sourceHash, normalizedRecords));
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
function evidenceNode(record, source) {
|
|
43
|
+
return {
|
|
44
|
+
id: (0, extractor_contract_1.createEvidenceNodeId)(record.id),
|
|
45
|
+
type: 'Evidence',
|
|
46
|
+
label: record.summary,
|
|
47
|
+
status: record.outcome,
|
|
48
|
+
kind: record.category,
|
|
49
|
+
metadata: {
|
|
50
|
+
taskId: record.taskId,
|
|
51
|
+
time: record.time,
|
|
52
|
+
idSource: record.idSource,
|
|
53
|
+
idStability: record.idStability,
|
|
54
|
+
persistedSchemaVersion: record.persistedSchemaVersion,
|
|
55
|
+
sourceLine: record.sourceLine ?? null,
|
|
56
|
+
fingerprint: record.fingerprint,
|
|
57
|
+
category: record.category,
|
|
58
|
+
artifactType: record.artifactType,
|
|
59
|
+
outcome: record.outcome,
|
|
60
|
+
visibility: record.visibility,
|
|
61
|
+
artifacts: record.artifacts,
|
|
62
|
+
tags: record.tags,
|
|
63
|
+
legacy: record.legacy
|
|
64
|
+
},
|
|
65
|
+
source
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function evidenceEdges(task, record, source) {
|
|
69
|
+
const taskNodeId = (0, extractor_contract_1.createTaskNodeId)(task.id);
|
|
70
|
+
const evidenceNodeId = (0, extractor_contract_1.createEvidenceNodeId)(record.id);
|
|
71
|
+
const edges = [
|
|
72
|
+
edge('HAS_EVIDENCE', taskNodeId, evidenceNodeId, source, `${task.id} has evidence ${record.id}.`)
|
|
73
|
+
];
|
|
74
|
+
if (record.tags.includes('close-proof')) {
|
|
75
|
+
edges.push(edge('CLOSES_WITH', taskNodeId, evidenceNodeId, source, `${task.id} closes with evidence ${record.id}.`));
|
|
76
|
+
}
|
|
77
|
+
for (const dependencyId of evidenceDependencyIds(record.tags)) {
|
|
78
|
+
edges.push(edge('DEPENDS_ON_EVIDENCE', evidenceNodeId, (0, extractor_contract_1.createEvidenceNodeId)(dependencyId), source, `${record.id} references evidence ${dependencyId}.`));
|
|
79
|
+
}
|
|
80
|
+
return edges;
|
|
81
|
+
}
|
|
82
|
+
function evidenceStateSource(task, relativePath, sourceHash, records) {
|
|
83
|
+
return {
|
|
84
|
+
id: `state-source:evidence:${task.id}`,
|
|
85
|
+
path: relativePath,
|
|
86
|
+
kind: 'evidence',
|
|
87
|
+
hash: sourceHash,
|
|
88
|
+
extracted: {
|
|
89
|
+
taskId: task.id,
|
|
90
|
+
records: records.length,
|
|
91
|
+
durableRecords: records.filter((record) => record.idStability === 'durable').length,
|
|
92
|
+
legacyRecords: records.filter((record) => record.persistedSchemaVersion === 'hadara.evidence.v1').length,
|
|
93
|
+
closeProofs: records.filter((record) => record.tags.includes('close-proof')).length,
|
|
94
|
+
categoryCounts: countBy(records.map((record) => record.category)),
|
|
95
|
+
outcomeCounts: countBy(records.map((record) => record.outcome))
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseEvidenceJsonl(content, relativePath, issues) {
|
|
100
|
+
const entries = [];
|
|
101
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
102
|
+
const lineNumber = index + 1;
|
|
103
|
+
if (!line.trim())
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
entries.push({ record: JSON.parse(line), lineNumber });
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
issues.push(parseFailedIssue(relativePath, lineNumber, `Could not parse evidence JSONL line ${lineNumber}: ${error instanceof Error ? error.message : String(error)}`));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
function normalizeEntry(entry, task, relativePath, issues) {
|
|
115
|
+
try {
|
|
116
|
+
return (0, normalizer_1.normalizeEvidenceRecord)(entry.record, {
|
|
117
|
+
taskDir: task.dir,
|
|
118
|
+
lineNumber: entry.lineNumber
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
issues.push(parseFailedIssue(relativePath, entry.lineNumber, `Could not normalize evidence JSONL line ${entry.lineNumber}: ${error instanceof Error ? error.message : String(error)}`));
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function evidenceDependencyIds(tags) {
|
|
127
|
+
const ids = new Set();
|
|
128
|
+
for (const tag of tags) {
|
|
129
|
+
const match = tag.match(/^(?:resolves|supersedes):(.+)$/);
|
|
130
|
+
if (match?.[1])
|
|
131
|
+
ids.add(match[1]);
|
|
132
|
+
}
|
|
133
|
+
return Array.from(ids).sort();
|
|
134
|
+
}
|
|
135
|
+
function edge(type, from, to, source, reason) {
|
|
136
|
+
return {
|
|
137
|
+
id: (0, extractor_contract_1.createContextGraphEdgeId)({ type, from, to, source, reason }),
|
|
138
|
+
from,
|
|
139
|
+
to,
|
|
140
|
+
type,
|
|
141
|
+
confidence: 'explicit',
|
|
142
|
+
reason,
|
|
143
|
+
source
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function readEvidenceSource(projectRoot, task) {
|
|
147
|
+
const absolutePath = node_path_1.default.join(task.dir, 'evidence.jsonl');
|
|
148
|
+
return {
|
|
149
|
+
path: (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, absolutePath),
|
|
150
|
+
content: readOptionalText(absolutePath)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function readOptionalText(absolutePath) {
|
|
154
|
+
return node_fs_1.default.existsSync(absolutePath) ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : null;
|
|
155
|
+
}
|
|
156
|
+
function evidenceReadFailedIssue(relativePath, message) {
|
|
157
|
+
return {
|
|
158
|
+
severity: 'warning',
|
|
159
|
+
code: 'CONTEXT_GRAPH_EVIDENCE_READ_FAILED',
|
|
160
|
+
path: relativePath,
|
|
161
|
+
message,
|
|
162
|
+
fixHint: `Restore ${relativePath} or run the scoped evidence remediation workflow before relying on evidence context routing.`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function parseFailedIssue(relativePath, lineNumber, message) {
|
|
166
|
+
return {
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
code: 'CONTEXT_GRAPH_PARSE_FAILED',
|
|
169
|
+
path: relativePath,
|
|
170
|
+
message,
|
|
171
|
+
fixHint: `Repair line ${lineNumber} in ${relativePath}; context graph extraction will skip malformed evidence records.`
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function countBy(values) {
|
|
175
|
+
return values.reduce((counts, value) => {
|
|
176
|
+
counts[value] = (counts[value] ?? 0) + 1;
|
|
177
|
+
return counts;
|
|
178
|
+
}, {});
|
|
179
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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.normalizeContextGraphPath = normalizeContextGraphPath;
|
|
7
|
+
exports.toProjectRelativeContextPath = toProjectRelativeContextPath;
|
|
8
|
+
exports.hashContextGraphText = hashContextGraphText;
|
|
9
|
+
exports.hashContextGraphJson = hashContextGraphJson;
|
|
10
|
+
exports.hashContextGraphSources = hashContextGraphSources;
|
|
11
|
+
exports.createContextGraphSourceRef = createContextGraphSourceRef;
|
|
12
|
+
exports.createTaskNodeId = createTaskNodeId;
|
|
13
|
+
exports.createDocumentNodeId = createDocumentNodeId;
|
|
14
|
+
exports.createManagedSectionNodeId = createManagedSectionNodeId;
|
|
15
|
+
exports.createCommandNodeId = createCommandNodeId;
|
|
16
|
+
exports.createEvidenceNodeId = createEvidenceNodeId;
|
|
17
|
+
exports.createReleaseCheckNodeId = createReleaseCheckNodeId;
|
|
18
|
+
exports.createDecisionNodeId = createDecisionNodeId;
|
|
19
|
+
exports.createKnownProblemNodeId = createKnownProblemNodeId;
|
|
20
|
+
exports.createContextGraphEdgeId = createContextGraphEdgeId;
|
|
21
|
+
exports.createEmptyExtractionResult = createEmptyExtractionResult;
|
|
22
|
+
exports.mergeGraphExtractionResults = mergeGraphExtractionResults;
|
|
23
|
+
exports.summarizeContextGraphExtraction = summarizeContextGraphExtraction;
|
|
24
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
26
|
+
const context_graph_1 = require("./context-graph");
|
|
27
|
+
function normalizeContextGraphPath(inputPath) {
|
|
28
|
+
const normalized = inputPath.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
29
|
+
return normalized.split('/').filter((part) => part.length > 0).join('/');
|
|
30
|
+
}
|
|
31
|
+
function toProjectRelativeContextPath(projectRoot, absoluteOrRelativePath) {
|
|
32
|
+
const resolvedRoot = node_path_1.default.resolve(projectRoot);
|
|
33
|
+
const resolvedPath = node_path_1.default.isAbsolute(absoluteOrRelativePath)
|
|
34
|
+
? node_path_1.default.resolve(absoluteOrRelativePath)
|
|
35
|
+
: node_path_1.default.resolve(resolvedRoot, absoluteOrRelativePath);
|
|
36
|
+
const relative = node_path_1.default.relative(resolvedRoot, resolvedPath);
|
|
37
|
+
return normalizeContextGraphPath(relative || '.');
|
|
38
|
+
}
|
|
39
|
+
function hashContextGraphText(value) {
|
|
40
|
+
return `sha256:${node_crypto_1.default.createHash('sha256').update(value, 'utf8').digest('hex')}`;
|
|
41
|
+
}
|
|
42
|
+
function hashContextGraphJson(value) {
|
|
43
|
+
return hashContextGraphText(stableStringify(value));
|
|
44
|
+
}
|
|
45
|
+
function hashContextGraphSources(sources) {
|
|
46
|
+
return hashContextGraphJson(sources.map((source) => ({
|
|
47
|
+
path: normalizeContextGraphPath(source.path),
|
|
48
|
+
hash: source.hash ?? (source.content == null ? null : hashContextGraphText(source.content))
|
|
49
|
+
})).sort((a, b) => a.path.localeCompare(b.path)));
|
|
50
|
+
}
|
|
51
|
+
function createContextGraphSourceRef(input) {
|
|
52
|
+
return {
|
|
53
|
+
path: normalizeContextGraphPath(input.path),
|
|
54
|
+
...(input.line === undefined ? {} : { line: input.line }),
|
|
55
|
+
...(input.hash || input.content != null ? { hash: input.hash ?? hashContextGraphText(input.content ?? '') } : {}),
|
|
56
|
+
extractor: input.extractor
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createTaskNodeId(taskId) {
|
|
60
|
+
return `task:${taskId}`;
|
|
61
|
+
}
|
|
62
|
+
function createDocumentNodeId(documentPath) {
|
|
63
|
+
return `doc:${normalizeContextGraphPath(documentPath)}`;
|
|
64
|
+
}
|
|
65
|
+
function createManagedSectionNodeId(documentPath, sectionId) {
|
|
66
|
+
return `section:${normalizeContextGraphPath(documentPath)}#${sectionId}`;
|
|
67
|
+
}
|
|
68
|
+
function createCommandNodeId(commandId) {
|
|
69
|
+
return `command:${commandId}`;
|
|
70
|
+
}
|
|
71
|
+
function createEvidenceNodeId(evidenceId) {
|
|
72
|
+
return evidenceId;
|
|
73
|
+
}
|
|
74
|
+
function createReleaseCheckNodeId(name) {
|
|
75
|
+
return `release-check:${normalizeIdPart(name)}`;
|
|
76
|
+
}
|
|
77
|
+
function createDecisionNodeId(documentPath, decisionId) {
|
|
78
|
+
return `decision:${normalizeContextGraphPath(documentPath)}#${decisionId}`;
|
|
79
|
+
}
|
|
80
|
+
function createKnownProblemNodeId(documentPath, text) {
|
|
81
|
+
return `known-problem:${sha256Hex(`${normalizeContextGraphPath(documentPath)}\n${normalizeWhitespace(text)}`)}`;
|
|
82
|
+
}
|
|
83
|
+
function createContextGraphEdgeId(input) {
|
|
84
|
+
return `edge:${input.type}:${sha256Hex(stableStringify({
|
|
85
|
+
from: input.from,
|
|
86
|
+
to: input.to,
|
|
87
|
+
source: {
|
|
88
|
+
path: normalizeContextGraphPath(input.source.path),
|
|
89
|
+
line: input.source.line ?? null,
|
|
90
|
+
extractor: input.source.extractor
|
|
91
|
+
},
|
|
92
|
+
reason: normalizeWhitespace(input.reason)
|
|
93
|
+
}))}`;
|
|
94
|
+
}
|
|
95
|
+
function createEmptyExtractionResult(extractor, sources = []) {
|
|
96
|
+
return {
|
|
97
|
+
source: {
|
|
98
|
+
extractor,
|
|
99
|
+
paths: sources.map((source) => normalizeContextGraphPath(source.path)).sort(),
|
|
100
|
+
sourceHash: hashContextGraphSources(sources)
|
|
101
|
+
},
|
|
102
|
+
nodes: [],
|
|
103
|
+
edges: [],
|
|
104
|
+
stateSources: [],
|
|
105
|
+
issues: []
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function mergeGraphExtractionResults(results) {
|
|
109
|
+
const sourceInputs = results.flatMap((result) => result.source.paths.map((sourcePath) => ({ path: sourcePath, hash: result.source.sourceHash })));
|
|
110
|
+
return {
|
|
111
|
+
source: {
|
|
112
|
+
extractor: 'mergeGraphExtractionResults',
|
|
113
|
+
paths: Array.from(new Set(results.flatMap((result) => result.source.paths))).sort(),
|
|
114
|
+
sourceHash: hashContextGraphSources(sourceInputs)
|
|
115
|
+
},
|
|
116
|
+
nodes: dedupeById(results.flatMap((result) => result.nodes)),
|
|
117
|
+
edges: dedupeById(results.flatMap((result) => result.edges)),
|
|
118
|
+
stateSources: dedupeStateSources(results.flatMap((result) => result.stateSources ?? [])),
|
|
119
|
+
issues: results.flatMap((result) => result.issues)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function summarizeContextGraphExtraction(nodes, edges, issues) {
|
|
123
|
+
return {
|
|
124
|
+
nodeCounts: countByVocabulary(context_graph_1.CONTEXT_GRAPH_NODE_TYPES, nodes.map((node) => node.type)),
|
|
125
|
+
edgeCounts: countByVocabulary(context_graph_1.CONTEXT_GRAPH_EDGE_TYPES, edges.map((edge) => edge.type)),
|
|
126
|
+
sourcesRead: new Set(nodes.map((node) => node.source.path).concat(edges.map((edge) => edge.source.path))).size,
|
|
127
|
+
degraded: issues.some((issue) => issue.severity === 'warning' || issue.severity === 'error')
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function dedupeById(items) {
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const deduped = [];
|
|
133
|
+
for (const item of items) {
|
|
134
|
+
if (seen.has(item.id))
|
|
135
|
+
continue;
|
|
136
|
+
seen.add(item.id);
|
|
137
|
+
deduped.push(item);
|
|
138
|
+
}
|
|
139
|
+
return deduped;
|
|
140
|
+
}
|
|
141
|
+
function dedupeStateSources(items) {
|
|
142
|
+
return dedupeById(items);
|
|
143
|
+
}
|
|
144
|
+
function countByVocabulary(vocabulary, values) {
|
|
145
|
+
const counts = Object.fromEntries(vocabulary.map((value) => [value, 0]));
|
|
146
|
+
for (const value of values)
|
|
147
|
+
counts[value] += 1;
|
|
148
|
+
return counts;
|
|
149
|
+
}
|
|
150
|
+
function stableStringify(value) {
|
|
151
|
+
if (value === null || typeof value !== 'object')
|
|
152
|
+
return JSON.stringify(value);
|
|
153
|
+
if (Array.isArray(value))
|
|
154
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
155
|
+
const record = value;
|
|
156
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(',')}}`;
|
|
157
|
+
}
|
|
158
|
+
function normalizeWhitespace(value) {
|
|
159
|
+
return value.trim().replace(/\s+/g, ' ');
|
|
160
|
+
}
|
|
161
|
+
function normalizeIdPart(value) {
|
|
162
|
+
return normalizeWhitespace(value).toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
|
|
163
|
+
}
|
|
164
|
+
function sha256Hex(value) {
|
|
165
|
+
return node_crypto_1.default.createHash('sha256').update(value, 'utf8').digest('hex');
|
|
166
|
+
}
|