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,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createContextStateProjectionReport = createContextStateProjectionReport;
|
|
4
|
+
const TASK_ID_PATTERN = /\bT-\d{4}\b/;
|
|
5
|
+
function createContextStateProjectionReport(input) {
|
|
6
|
+
const stateSources = input.extractionResults.flatMap((result) => result.stateSources ?? []);
|
|
7
|
+
const nodes = input.extractionResults.flatMap((result) => result.nodes);
|
|
8
|
+
const graphIssues = input.extractionResults.flatMap((result) => result.issues);
|
|
9
|
+
const taskIndex = indexTaskNodes(nodes);
|
|
10
|
+
const latestCompleted = chooseTaskHint(latestCompletedCandidates(stateSources));
|
|
11
|
+
const active = chooseTaskHint(activeTaskCandidates(stateSources));
|
|
12
|
+
const latestClosed = latestClosedTask(stateSources);
|
|
13
|
+
const issues = [
|
|
14
|
+
...stateSourceConsistencyIssues(stateSources),
|
|
15
|
+
...taskPresenceIssues(taskIndex),
|
|
16
|
+
...closeProofIssues(stateSources),
|
|
17
|
+
...graphIssues.map(graphIssueToStateIssue)
|
|
18
|
+
].filter((issue) => Boolean(issue));
|
|
19
|
+
return {
|
|
20
|
+
schemaVersion: 'hadara.stateProjection.v1',
|
|
21
|
+
command: 'state.projection',
|
|
22
|
+
ok: issues.every((issue) => issue.severity !== 'error'),
|
|
23
|
+
generatedAt: input.generatedAt,
|
|
24
|
+
summary: {
|
|
25
|
+
...(latestCompleted ? { latestCompletedTask: latestCompleted } : {}),
|
|
26
|
+
...(active ? { activeTask: active } : {}),
|
|
27
|
+
...(latestClosed ? { latestClosedTask: latestClosed } : {}),
|
|
28
|
+
releaseState: releaseState(stateSources),
|
|
29
|
+
stateConsistency: stateConsistency(issues)
|
|
30
|
+
},
|
|
31
|
+
sources: dedupeStateSources(stateSources),
|
|
32
|
+
issues
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function stateSourceConsistencyIssues(stateSources) {
|
|
36
|
+
const issues = [];
|
|
37
|
+
const latestCandidates = latestCompletedCandidates(stateSources);
|
|
38
|
+
const activeCandidates = activeTaskCandidates(stateSources);
|
|
39
|
+
if (uniqueTaskIds(latestCandidates).length > 1) {
|
|
40
|
+
issues.push(issue('warning', 'STATE_LATEST_TASK_MISMATCH', `Latest completed task differs across state sources: ${formatCandidates(latestCandidates)}.`, latestCandidates.map((candidate) => candidate.path), 'Update PROJECT_STATE, AGENT_HANDOFF, and Task Board latest Done state so they point at the same task.'));
|
|
41
|
+
}
|
|
42
|
+
if (uniqueTaskIds(activeCandidates).length > 1) {
|
|
43
|
+
issues.push(issue('warning', 'STATE_ACTIVE_TASK_MISMATCH', `Active task differs across state sources: ${formatCandidates(activeCandidates)}.`, activeCandidates.map((candidate) => candidate.path), 'Update active task state in PROJECT_STATE, AGENT_HANDOFF, and Task Board, or clear it when no concrete task is active.'));
|
|
44
|
+
}
|
|
45
|
+
if (!stateSources.some((source) => source.kind === 'release-readiness')) {
|
|
46
|
+
issues.push(issue('warning', 'STATE_RELEASE_EVIDENCE_STALE', 'Release readiness state source is missing from context extraction.', ['docs/RELEASE_READINESS.md'], 'Run or include release readiness extraction before relying on release-state context routing.'));
|
|
47
|
+
}
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
function taskPresenceIssues(taskIndex) {
|
|
51
|
+
const issues = [];
|
|
52
|
+
for (const taskId of taskIndex.taskBoardTaskIds) {
|
|
53
|
+
if (taskIndex.taskCapsuleTaskIds.has(taskId))
|
|
54
|
+
continue;
|
|
55
|
+
issues.push(issue('warning', 'STATE_TASK_CAPSULE_MISSING', `Task Board references ${taskId}, but no matching Task Capsule node was extracted.`, [taskIndex.taskBoardPaths.get(taskId) ?? 'docs/TASK_BOARD.md'], `Create the missing ${taskId} capsule or update the Task Board row.`));
|
|
56
|
+
}
|
|
57
|
+
for (const taskId of taskIndex.taskCapsuleTaskIds) {
|
|
58
|
+
if (taskIndex.taskBoardTaskIds.has(taskId))
|
|
59
|
+
continue;
|
|
60
|
+
issues.push(issue('warning', 'STATE_TASK_BOARD_MISSING_ROW', `Task Capsule ${taskId} exists, but no matching Task Board row was extracted.`, [taskIndex.taskCapsulePaths.get(taskId) ?? `tasks/${taskId}/TASK.md`, 'docs/TASK_BOARD.md'], `Add or repair the Task Board row for ${taskId}.`));
|
|
61
|
+
}
|
|
62
|
+
return issues;
|
|
63
|
+
}
|
|
64
|
+
function closeProofIssues(stateSources) {
|
|
65
|
+
const latest = chooseTaskHint(latestCompletedCandidates(stateSources));
|
|
66
|
+
const closed = latestClosedTask(stateSources);
|
|
67
|
+
if (!latest || closed === latest)
|
|
68
|
+
return [];
|
|
69
|
+
return [issue('warning', 'STATE_CLOSE_PROOF_STALE', closed
|
|
70
|
+
? `Latest close proof is ${closed}, but latest completed task is ${latest}.`
|
|
71
|
+
: `No close proof state source was extracted for latest completed task ${latest}.`, evidenceSourcePaths(stateSources, latest), `Run task ready/close/audit-close for ${latest}, or include current evidence extraction before relying on close-proof state.`)];
|
|
72
|
+
}
|
|
73
|
+
function graphIssueToStateIssue(graphIssue) {
|
|
74
|
+
if (graphIssue.severity === 'info')
|
|
75
|
+
return null;
|
|
76
|
+
return issue(graphIssue.severity, 'STATE_UNKNOWN', graphIssue.message, graphIssue.path ? [graphIssue.path] : [], graphIssue.fixHint ?? 'Resolve the underlying context graph extraction issue.');
|
|
77
|
+
}
|
|
78
|
+
function latestCompletedCandidates(stateSources) {
|
|
79
|
+
return [
|
|
80
|
+
candidateFromSource(stateSources, 'task-board', 'latestDoneTask'),
|
|
81
|
+
candidateFromSource(stateSources, 'project-state', 'latestCompletedTask'),
|
|
82
|
+
candidateFromSource(stateSources, 'agent-handoff', 'latestCompletedTask')
|
|
83
|
+
].filter((candidate) => Boolean(candidate?.taskId));
|
|
84
|
+
}
|
|
85
|
+
function activeTaskCandidates(stateSources) {
|
|
86
|
+
const taskBoard = stateSources.find((source) => source.kind === 'task-board');
|
|
87
|
+
const activeTasks = Array.isArray(taskBoard?.extracted.activeTasks)
|
|
88
|
+
? (taskBoard?.extracted.activeTasks).map((value) => normalizeTaskId(String(value))).filter((value) => Boolean(value))
|
|
89
|
+
: [];
|
|
90
|
+
return [
|
|
91
|
+
activeTasks.length === 1 && taskBoard ? { source: taskBoard.kind, path: taskBoard.path, taskId: activeTasks[0] } : null,
|
|
92
|
+
candidateFromSource(stateSources, 'project-state', 'activeTask'),
|
|
93
|
+
candidateFromSource(stateSources, 'agent-handoff', 'activeTask')
|
|
94
|
+
].filter((candidate) => Boolean(candidate?.taskId));
|
|
95
|
+
}
|
|
96
|
+
function candidateFromSource(stateSources, kind, field) {
|
|
97
|
+
const source = stateSources.find((item) => item.kind === kind);
|
|
98
|
+
if (!source)
|
|
99
|
+
return null;
|
|
100
|
+
const taskId = normalizeTaskId(source.extracted[field]);
|
|
101
|
+
return taskId ? { source: kind, path: source.path, taskId } : null;
|
|
102
|
+
}
|
|
103
|
+
function chooseTaskHint(candidates) {
|
|
104
|
+
return candidates[0]?.taskId ?? null;
|
|
105
|
+
}
|
|
106
|
+
function uniqueTaskIds(candidates) {
|
|
107
|
+
return Array.from(new Set(candidates.map((candidate) => candidate.taskId).filter((value) => Boolean(value)))).sort();
|
|
108
|
+
}
|
|
109
|
+
function formatCandidates(candidates) {
|
|
110
|
+
return candidates.map((candidate) => `${candidate.source}=${candidate.taskId}`).join(', ');
|
|
111
|
+
}
|
|
112
|
+
function normalizeTaskId(value) {
|
|
113
|
+
if (typeof value !== 'string')
|
|
114
|
+
return null;
|
|
115
|
+
if (/^(none|n\/a|tbd)\b/i.test(value.trim()))
|
|
116
|
+
return null;
|
|
117
|
+
return value.match(TASK_ID_PATTERN)?.[0] ?? null;
|
|
118
|
+
}
|
|
119
|
+
function latestClosedTask(stateSources) {
|
|
120
|
+
return stateSources
|
|
121
|
+
.filter((source) => source.kind === 'evidence' && Number(source.extracted.closeProofs ?? 0) > 0)
|
|
122
|
+
.map((source) => normalizeTaskId(source.extracted.taskId))
|
|
123
|
+
.filter((taskId) => Boolean(taskId))
|
|
124
|
+
.sort()
|
|
125
|
+
.at(-1) ?? null;
|
|
126
|
+
}
|
|
127
|
+
function releaseState(stateSources) {
|
|
128
|
+
const source = stateSources.find((item) => item.kind === 'release-readiness');
|
|
129
|
+
if (!source)
|
|
130
|
+
return 'unknown';
|
|
131
|
+
const checks = Number(source.extracted.checks ?? 0);
|
|
132
|
+
if (checks <= 0)
|
|
133
|
+
return 'unknown';
|
|
134
|
+
const statusCounts = source.extracted.statusCounts;
|
|
135
|
+
if (isRecord(statusCounts) && Number(statusCounts.blocked ?? 0) > 0)
|
|
136
|
+
return 'blocked';
|
|
137
|
+
if (isRecord(statusCounts) && Number(statusCounts.current ?? 0) > 0)
|
|
138
|
+
return 'current';
|
|
139
|
+
return 'documented';
|
|
140
|
+
}
|
|
141
|
+
function indexTaskNodes(nodes) {
|
|
142
|
+
const taskBoardTaskIds = new Set();
|
|
143
|
+
const taskCapsuleTaskIds = new Set();
|
|
144
|
+
const taskBoardPaths = new Map();
|
|
145
|
+
const taskCapsulePaths = new Map();
|
|
146
|
+
for (const node of nodes) {
|
|
147
|
+
if (node.type !== 'Task')
|
|
148
|
+
continue;
|
|
149
|
+
const taskId = normalizeTaskId(node.id);
|
|
150
|
+
if (!taskId)
|
|
151
|
+
continue;
|
|
152
|
+
if (node.kind === 'task-board-row') {
|
|
153
|
+
taskBoardTaskIds.add(taskId);
|
|
154
|
+
taskBoardPaths.set(taskId, node.source.path);
|
|
155
|
+
}
|
|
156
|
+
if (node.kind === 'task-capsule') {
|
|
157
|
+
taskCapsuleTaskIds.add(taskId);
|
|
158
|
+
taskCapsulePaths.set(taskId, node.path ?? node.source.path);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { taskBoardTaskIds, taskCapsuleTaskIds, taskBoardPaths, taskCapsulePaths };
|
|
162
|
+
}
|
|
163
|
+
function evidenceSourcePaths(stateSources, latestTaskId) {
|
|
164
|
+
const paths = stateSources.filter((source) => source.kind === 'evidence').map((source) => source.path);
|
|
165
|
+
return paths.length > 0 ? paths : [`tasks/${latestTaskId}/evidence.jsonl`];
|
|
166
|
+
}
|
|
167
|
+
function stateConsistency(issues) {
|
|
168
|
+
if (issues.some((item) => item.severity === 'error'))
|
|
169
|
+
return 'error';
|
|
170
|
+
if (issues.some((item) => item.severity === 'warning'))
|
|
171
|
+
return 'warning';
|
|
172
|
+
return 'consistent';
|
|
173
|
+
}
|
|
174
|
+
function dedupeStateSources(stateSources) {
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
const result = [];
|
|
177
|
+
for (const source of stateSources) {
|
|
178
|
+
if (seen.has(source.id))
|
|
179
|
+
continue;
|
|
180
|
+
seen.add(source.id);
|
|
181
|
+
result.push(source);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
function issue(severity, code, message, paths, fixHint) {
|
|
186
|
+
return {
|
|
187
|
+
severity,
|
|
188
|
+
code,
|
|
189
|
+
message,
|
|
190
|
+
paths: Array.from(new Set(paths)).sort(),
|
|
191
|
+
fixHint
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function isRecord(value) {
|
|
195
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
196
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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.extractTaskBoard = extractTaskBoard;
|
|
7
|
+
exports.extractTaskCapsules = extractTaskCapsules;
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const extractor_contract_1 = require("./extractor-contract");
|
|
11
|
+
const markdown_table_1 = require("../services/markdown-table");
|
|
12
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
13
|
+
function extractTaskBoard(projectRoot) {
|
|
14
|
+
const relativePath = 'docs/TASK_BOARD.md';
|
|
15
|
+
const absolutePath = node_path_1.default.join(projectRoot, relativePath);
|
|
16
|
+
const content = readOptionalText(absolutePath);
|
|
17
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractTaskBoard', [{ path: relativePath, content }]);
|
|
18
|
+
if (content == null) {
|
|
19
|
+
result.issues.push(sourceMissingIssue(relativePath, 'Task Board is missing; task graph nodes cannot be extracted from docs/TASK_BOARD.md.'));
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
const sourceHash = (0, extractor_contract_1.hashContextGraphText)(content);
|
|
23
|
+
const rows = parseTaskBoardRows(content);
|
|
24
|
+
result.nodes.push(...rows.map((row) => taskBoardNode(row, sourceHash)));
|
|
25
|
+
result.stateSources?.push(taskBoardStateSource(rows, sourceHash));
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function extractTaskCapsules(projectRoot) {
|
|
29
|
+
const capsules = (0, task_capsule_1.listTaskCapsules)(projectRoot);
|
|
30
|
+
const sources = capsules.flatMap((task) => [
|
|
31
|
+
readSourceInput(projectRoot, node_path_1.default.join(task.dir, 'TASK.md')),
|
|
32
|
+
readSourceInput(projectRoot, node_path_1.default.join(task.dir, 'HANDOFF.md'))
|
|
33
|
+
]);
|
|
34
|
+
const result = (0, extractor_contract_1.createEmptyExtractionResult)('extractTaskCapsules', sources);
|
|
35
|
+
const stateSources = [];
|
|
36
|
+
for (const task of capsules) {
|
|
37
|
+
const taskPath = (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, node_path_1.default.join(task.dir, 'TASK.md'));
|
|
38
|
+
const taskContent = readOptionalText(node_path_1.default.join(task.dir, 'TASK.md'));
|
|
39
|
+
if (taskContent == null) {
|
|
40
|
+
result.issues.push(sourceMissingIssue(taskPath, `Task Capsule ${task.id} is missing TASK.md.`));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const handoffPath = (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, node_path_1.default.join(task.dir, 'HANDOFF.md'));
|
|
44
|
+
const handoffContent = readOptionalText(node_path_1.default.join(task.dir, 'HANDOFF.md'));
|
|
45
|
+
const taskStatus = readTaskStatus(taskContent);
|
|
46
|
+
const handoffStatus = handoffContent == null ? null : readTaskHandoffStatus(handoffContent);
|
|
47
|
+
const taskHash = (0, extractor_contract_1.hashContextGraphText)(taskContent);
|
|
48
|
+
const source = (0, extractor_contract_1.createContextGraphSourceRef)({
|
|
49
|
+
path: taskPath,
|
|
50
|
+
extractor: 'extractTaskCapsules',
|
|
51
|
+
line: 1,
|
|
52
|
+
hash: taskHash
|
|
53
|
+
});
|
|
54
|
+
result.nodes.push({
|
|
55
|
+
id: (0, extractor_contract_1.createTaskNodeId)(task.id),
|
|
56
|
+
type: 'Task',
|
|
57
|
+
label: `${task.id} ${task.title}`,
|
|
58
|
+
path: taskPath,
|
|
59
|
+
status: taskStatus ?? undefined,
|
|
60
|
+
kind: 'task-capsule',
|
|
61
|
+
metadata: {
|
|
62
|
+
capsule: (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, task.dir),
|
|
63
|
+
...(handoffStatus ? { handoffTaskStatus: handoffStatus } : {}),
|
|
64
|
+
handoffPath,
|
|
65
|
+
handoffPresent: handoffContent != null
|
|
66
|
+
},
|
|
67
|
+
source
|
|
68
|
+
});
|
|
69
|
+
stateSources.push({
|
|
70
|
+
id: `state-source:task-capsule:${task.id}`,
|
|
71
|
+
path: taskPath,
|
|
72
|
+
kind: 'task-capsule',
|
|
73
|
+
hash: taskHash,
|
|
74
|
+
extracted: {
|
|
75
|
+
taskId: task.id,
|
|
76
|
+
title: task.title,
|
|
77
|
+
status: taskStatus,
|
|
78
|
+
handoffTaskStatus: handoffStatus,
|
|
79
|
+
capsule: (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, task.dir)
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
result.stateSources?.push(...stateSources);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
function taskBoardNode(row, sourceHash) {
|
|
87
|
+
return {
|
|
88
|
+
id: (0, extractor_contract_1.createTaskNodeId)(row.id),
|
|
89
|
+
type: 'Task',
|
|
90
|
+
label: `${row.id} ${row.title}`.trim(),
|
|
91
|
+
path: (0, extractor_contract_1.normalizeContextGraphPath)(node_path_1.default.posix.join(row.capsule, 'TASK.md')),
|
|
92
|
+
status: row.status,
|
|
93
|
+
kind: 'task-board-row',
|
|
94
|
+
metadata: {
|
|
95
|
+
capsule: (0, extractor_contract_1.normalizeContextGraphPath)(row.capsule)
|
|
96
|
+
},
|
|
97
|
+
source: (0, extractor_contract_1.createContextGraphSourceRef)({
|
|
98
|
+
path: 'docs/TASK_BOARD.md',
|
|
99
|
+
extractor: 'extractTaskBoard',
|
|
100
|
+
line: row.line,
|
|
101
|
+
hash: sourceHash
|
|
102
|
+
})
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function taskBoardStateSource(rows, sourceHash) {
|
|
106
|
+
return {
|
|
107
|
+
id: 'state-source:task-board',
|
|
108
|
+
path: 'docs/TASK_BOARD.md',
|
|
109
|
+
kind: 'task-board',
|
|
110
|
+
hash: sourceHash,
|
|
111
|
+
extracted: {
|
|
112
|
+
rows: rows.length,
|
|
113
|
+
latestDoneTask: latestTaskId(rows.filter((row) => row.status === 'Done').map((row) => row.id)),
|
|
114
|
+
activeTasks: rows.filter((row) => row.status === 'In Progress').map((row) => row.id)
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function parseTaskBoardRows(content) {
|
|
119
|
+
const rows = [];
|
|
120
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (!trimmed.startsWith('|') || !trimmed.endsWith('|') || /^\|\s*:?-+/.test(trimmed))
|
|
123
|
+
return;
|
|
124
|
+
const cells = trimmed.slice(1, -1).split('|').map((cell) => cell.trim());
|
|
125
|
+
if (!/^T-\d{4}$/.test(cells[0] ?? ''))
|
|
126
|
+
return;
|
|
127
|
+
rows.push({
|
|
128
|
+
id: cells[0],
|
|
129
|
+
title: cells[1] ?? '',
|
|
130
|
+
status: cells[2] ?? '',
|
|
131
|
+
capsule: cells[3] ?? '',
|
|
132
|
+
line: index + 1
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
return rows;
|
|
136
|
+
}
|
|
137
|
+
function readTaskStatus(content) {
|
|
138
|
+
const sectionStatus = (0, markdown_table_1.readMarkdownSection)(content, '## Status').trim().split(/\r?\n/)[0]?.trim();
|
|
139
|
+
if (sectionStatus)
|
|
140
|
+
return sectionStatus;
|
|
141
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(content, '## Metadata');
|
|
142
|
+
return (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Status')?.[1] ?? null;
|
|
143
|
+
}
|
|
144
|
+
function readTaskHandoffStatus(content) {
|
|
145
|
+
const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(content, '## Current State');
|
|
146
|
+
return (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'TaskStatus')?.[1] ?? (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Status')?.[1] ?? null;
|
|
147
|
+
}
|
|
148
|
+
function readSourceInput(projectRoot, absolutePath) {
|
|
149
|
+
return {
|
|
150
|
+
path: (0, extractor_contract_1.toProjectRelativeContextPath)(projectRoot, absolutePath),
|
|
151
|
+
content: readOptionalText(absolutePath)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function readOptionalText(absolutePath) {
|
|
155
|
+
return node_fs_1.default.existsSync(absolutePath) ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : null;
|
|
156
|
+
}
|
|
157
|
+
function sourceMissingIssue(relativePath, message) {
|
|
158
|
+
return {
|
|
159
|
+
severity: 'warning',
|
|
160
|
+
code: 'CONTEXT_GRAPH_SOURCE_MISSING',
|
|
161
|
+
path: relativePath,
|
|
162
|
+
message,
|
|
163
|
+
fixHint: `Restore ${relativePath} or run the relevant HADARA workflow before relying on context graph extraction.`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function latestTaskId(ids) {
|
|
167
|
+
return ids.sort().at(-1) ?? null;
|
|
168
|
+
}
|
package/dist/core/schema.js
CHANGED
|
@@ -12,6 +12,14 @@ const actor_context_schema_json_1 = __importDefault(require("../schemas/actor-co
|
|
|
12
12
|
const active_run_projection_schema_json_1 = __importDefault(require("../schemas/active-run-projection.schema.json"));
|
|
13
13
|
const active_run_resume_schema_json_1 = __importDefault(require("../schemas/active-run-resume.schema.json"));
|
|
14
14
|
const clean_checkout_smoke_schema_json_1 = __importDefault(require("../schemas/clean-checkout-smoke.schema.json"));
|
|
15
|
+
const code_index_schema_json_1 = __importDefault(require("../schemas/code-index.schema.json"));
|
|
16
|
+
const context_cache_record_schema_json_1 = __importDefault(require("../schemas/context-cache-record.schema.json"));
|
|
17
|
+
const context_cache_status_schema_json_1 = __importDefault(require("../schemas/context-cache-status.schema.json"));
|
|
18
|
+
const context_cache_warm_schema_json_1 = __importDefault(require("../schemas/context-cache-warm.schema.json"));
|
|
19
|
+
const context_source_manifest_schema_json_1 = __importDefault(require("../schemas/context-source-manifest.schema.json"));
|
|
20
|
+
const context_pack_schema_json_1 = __importDefault(require("../schemas/context-pack.schema.json"));
|
|
21
|
+
const context_slice_schema_json_1 = __importDefault(require("../schemas/context-slice.schema.json"));
|
|
22
|
+
const context_graph_schema_json_1 = __importDefault(require("../schemas/context-graph.schema.json"));
|
|
15
23
|
const context_export_schema_json_1 = __importDefault(require("../schemas/context-export.schema.json"));
|
|
16
24
|
const dashboard_bootstrap_schema_json_1 = __importDefault(require("../schemas/dashboard-bootstrap.schema.json"));
|
|
17
25
|
const dashboard_core_schema_json_1 = __importDefault(require("../schemas/dashboard-core.schema.json"));
|
|
@@ -48,13 +56,18 @@ const release_dry_run_schema_json_1 = __importDefault(require("../schemas/releas
|
|
|
48
56
|
const release_gate_schema_json_1 = __importDefault(require("../schemas/release-gate.schema.json"));
|
|
49
57
|
const release_publish_schema_json_1 = __importDefault(require("../schemas/release-publish.schema.json"));
|
|
50
58
|
const runtime_version_schema_json_1 = __importDefault(require("../schemas/runtime-version.schema.json"));
|
|
59
|
+
const session_start_schema_json_1 = __importDefault(require("../schemas/session-start.schema.json"));
|
|
51
60
|
const smoke_evidence_summary_schema_json_1 = __importDefault(require("../schemas/smoke-evidence-summary.schema.json"));
|
|
52
61
|
const state_projection_schema_json_1 = __importDefault(require("../schemas/state-projection.schema.json"));
|
|
53
62
|
const task_audit_close_schema_json_1 = __importDefault(require("../schemas/task-audit-close.schema.json"));
|
|
54
63
|
const task_close_schema_json_1 = __importDefault(require("../schemas/task-close.schema.json"));
|
|
64
|
+
const task_close_repair_plan_schema_json_1 = __importDefault(require("../schemas/task-close-repair-plan.schema.json"));
|
|
55
65
|
const task_complete_flow_schema_json_1 = __importDefault(require("../schemas/task-complete-flow.schema.json"));
|
|
66
|
+
const task_context_schema_json_1 = __importDefault(require("../schemas/task-context.schema.json"));
|
|
56
67
|
const task_create_schema_json_1 = __importDefault(require("../schemas/task-create.schema.json"));
|
|
68
|
+
const task_finalize_schema_json_1 = __importDefault(require("../schemas/task-finalize.schema.json"));
|
|
57
69
|
const task_finish_schema_json_1 = __importDefault(require("../schemas/task-finish.schema.json"));
|
|
70
|
+
const task_lifecycle_schema_json_1 = __importDefault(require("../schemas/task-lifecycle.schema.json"));
|
|
58
71
|
const task_next_schema_json_1 = __importDefault(require("../schemas/task-next.schema.json"));
|
|
59
72
|
const task_ready_schema_json_1 = __importDefault(require("../schemas/task-ready.schema.json"));
|
|
60
73
|
const task_upgrade_scaffold_schema_json_1 = __importDefault(require("../schemas/task-upgrade-scaffold.schema.json"));
|
|
@@ -79,7 +92,15 @@ const registeredSchemas = {
|
|
|
79
92
|
'hadara.active_run.projection.v1': active_run_projection_schema_json_1.default,
|
|
80
93
|
'hadara.active_run.resume.v1': active_run_resume_schema_json_1.default,
|
|
81
94
|
'hadara.cleanCheckoutSmoke.v1': clean_checkout_smoke_schema_json_1.default,
|
|
95
|
+
'hadara.codeIndex.v1': code_index_schema_json_1.default,
|
|
96
|
+
'hadara.context.cacheRecord.v1': context_cache_record_schema_json_1.default,
|
|
97
|
+
'hadara.context.cacheStatus.v1': context_cache_status_schema_json_1.default,
|
|
98
|
+
'hadara.context.cacheWarm.v1': context_cache_warm_schema_json_1.default,
|
|
99
|
+
'hadara.context.sourceManifest.v1': context_source_manifest_schema_json_1.default,
|
|
100
|
+
'hadara.contextPack.v1': context_pack_schema_json_1.default,
|
|
101
|
+
'hadara.contextSlice.v1': context_slice_schema_json_1.default,
|
|
82
102
|
'hadara.context.export.v1': context_export_schema_json_1.default,
|
|
103
|
+
'hadara.contextGraph.v1': context_graph_schema_json_1.default,
|
|
83
104
|
'hadara.dashboard.bootstrap.v1': dashboard_bootstrap_schema_json_1.default,
|
|
84
105
|
'hadara.dashboard.core.v1': dashboard_core_schema_json_1.default,
|
|
85
106
|
'hadara.dashboard.task_detail.v1': dashboard_task_detail_schema_json_1.default,
|
|
@@ -115,13 +136,18 @@ const registeredSchemas = {
|
|
|
115
136
|
'hadara.releaseGate.v1': release_gate_schema_json_1.default,
|
|
116
137
|
'hadara.releasePublish.v1': release_publish_schema_json_1.default,
|
|
117
138
|
'hadara.runtime.version.v1': runtime_version_schema_json_1.default,
|
|
139
|
+
'hadara.sessionStart.v1': session_start_schema_json_1.default,
|
|
118
140
|
'hadara.smokeEvidenceSummary.v1': smoke_evidence_summary_schema_json_1.default,
|
|
119
141
|
'hadara.stateProjection.v1': state_projection_schema_json_1.default,
|
|
120
142
|
'hadara.task.audit_close.v1': task_audit_close_schema_json_1.default,
|
|
121
143
|
'hadara.task.close.v1': task_close_schema_json_1.default,
|
|
144
|
+
'hadara.task.closeRepairPlan.v1': task_close_repair_plan_schema_json_1.default,
|
|
122
145
|
'hadara.task.complete_flow.v1': task_complete_flow_schema_json_1.default,
|
|
146
|
+
'hadara.taskContext.v1': task_context_schema_json_1.default,
|
|
123
147
|
'hadara.task.create.v1': task_create_schema_json_1.default,
|
|
148
|
+
'hadara.task.finalize.v1': task_finalize_schema_json_1.default,
|
|
124
149
|
'hadara.task.finish.v1': task_finish_schema_json_1.default,
|
|
150
|
+
'hadara.task.lifecycle.v1': task_lifecycle_schema_json_1.default,
|
|
125
151
|
'hadara.task.next.v1': task_next_schema_json_1.default,
|
|
126
152
|
'hadara.task.ready.v1': task_ready_schema_json_1.default,
|
|
127
153
|
'hadara.task.upgrade_scaffold.v1': task_upgrade_scaffold_schema_json_1.default,
|
package/dist/harness/validate.js
CHANGED
|
@@ -8,6 +8,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
8
8
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
9
|
const markdown_table_1 = require("../services/markdown-table");
|
|
10
10
|
const evidence_lint_1 = require("../services/evidence-lint");
|
|
11
|
+
const acceptance_1 = require("../task/acceptance");
|
|
11
12
|
const task_capsule_1 = require("../task/task-capsule");
|
|
12
13
|
const REQUIRED_TASK_FILES = [
|
|
13
14
|
'TASK.md',
|
|
@@ -127,7 +128,7 @@ function validateTaskMarkdown(projectRoot, task, issues) {
|
|
|
127
128
|
function validateCapsuleFormatMarkdown(projectRoot, task, issues) {
|
|
128
129
|
validateMarkdownFile(projectRoot, task, issues, 'ACCEPTANCE.md', [
|
|
129
130
|
{ code: 'ACCEPTANCE_HEADING_INVALID', anyText: ['# Acceptance Criteria'] },
|
|
130
|
-
{ code: 'ACCEPTANCE_CHECKLIST_MISSING', anyText: ['- [ ]', '- [x]', '| ID | Criterion | Status | Evidence |'] }
|
|
131
|
+
{ code: 'ACCEPTANCE_CHECKLIST_MISSING', anyText: ['- [ ]', '- [x]', '| ID | Criterion | Status | Evidence |', '| ID | Criterion | Origin | Required | Deferrable | Status | Evidence |'] }
|
|
131
132
|
]);
|
|
132
133
|
validateMarkdownFile(projectRoot, task, issues, 'FILES.md', [
|
|
133
134
|
{ code: 'FILES_TABLE_INVALID', anyText: ['| Path | Action | Reason |', '| Path | Action | Reason | Status |'] },
|
|
@@ -455,18 +456,16 @@ function validateAcceptanceDone(projectRoot, task, issues) {
|
|
|
455
456
|
const checklistLines = content
|
|
456
457
|
.split(/\r?\n/)
|
|
457
458
|
.filter((line) => /^-\s+\[[ xX]\]/.test(line.trim()));
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
const status = cells[2]?.trim().toLowerCase();
|
|
462
|
-
return status === 'pending' || status === 'blocked' || !status;
|
|
463
|
-
});
|
|
459
|
+
const acceptance = (0, acceptance_1.analyzeAcceptanceReadiness)(content);
|
|
460
|
+
const tableRows = acceptance.rows;
|
|
461
|
+
const tableIncomplete = tableRows.length > 0 && acceptance.blockers.length > 0;
|
|
464
462
|
const checklistIncomplete = checklistLines.length > 0 && checklistLines.some((line) => /^-\s+\[\s\]/.test(line.trim()));
|
|
465
463
|
if ((tableRows.length > 0 && tableIncomplete) || (tableRows.length === 0 && (checklistLines.length === 0 || checklistIncomplete))) {
|
|
464
|
+
const blockerSummary = acceptance.blockers.length > 0 ? ` Blockers: ${acceptance.blockers.map((blocker) => `${blocker.code}(${blocker.row.id})`).join(', ')}.` : '';
|
|
466
465
|
issues.push({
|
|
467
466
|
severity: 'error',
|
|
468
467
|
code: 'ACCEPTANCE_INCOMPLETE',
|
|
469
|
-
message:
|
|
468
|
+
message: `Done-level validation requires all acceptance criteria to be complete.${blockerSummary}`,
|
|
470
469
|
path: relativePath,
|
|
471
470
|
heading: 'Acceptance Criteria',
|
|
472
471
|
fixHint: 'Mark each acceptance criterion complete with concrete evidence, or replace placeholder checklist rows with completed task-specific criteria.',
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "hadara.codeIndex.v1",
|
|
4
|
+
"x-hadara-schema-id": "hadara.codeIndex.v1",
|
|
5
|
+
"title": "HADARA Code Index Report",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": [
|
|
9
|
+
"schemaVersion",
|
|
10
|
+
"command",
|
|
11
|
+
"ok",
|
|
12
|
+
"generatedAt",
|
|
13
|
+
"projectRoot",
|
|
14
|
+
"sourceHash",
|
|
15
|
+
"files",
|
|
16
|
+
"symbols",
|
|
17
|
+
"edges",
|
|
18
|
+
"summary",
|
|
19
|
+
"issues"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"schemaVersion": { "const": "hadara.codeIndex.v1" },
|
|
23
|
+
"command": { "const": "code.index" },
|
|
24
|
+
"ok": { "type": "boolean" },
|
|
25
|
+
"generatedAt": { "type": "string" },
|
|
26
|
+
"projectRoot": { "type": "string" },
|
|
27
|
+
"sourceHash": { "type": "string", "minLength": 1 },
|
|
28
|
+
"files": { "type": "array", "items": { "$ref": "#/$defs/file" } },
|
|
29
|
+
"symbols": { "type": "array", "items": { "$ref": "#/$defs/symbol" } },
|
|
30
|
+
"edges": { "type": "array", "items": { "$ref": "#/$defs/edge" } },
|
|
31
|
+
"summary": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": true,
|
|
34
|
+
"required": ["sourceFiles", "testFiles", "fixtureFiles", "configFiles", "symbols", "edges", "degraded"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"sourceFiles": { "type": "number" },
|
|
37
|
+
"testFiles": { "type": "number" },
|
|
38
|
+
"fixtureFiles": { "type": "number" },
|
|
39
|
+
"configFiles": { "type": "number" },
|
|
40
|
+
"symbols": { "type": "number" },
|
|
41
|
+
"edges": { "type": "number" },
|
|
42
|
+
"degraded": { "type": "boolean" }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"budget": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": true,
|
|
48
|
+
"required": [
|
|
49
|
+
"maxIndexedFiles",
|
|
50
|
+
"maxIndexedBytes",
|
|
51
|
+
"maxSingleFileBytes",
|
|
52
|
+
"indexedFiles",
|
|
53
|
+
"indexedBytes",
|
|
54
|
+
"skippedFiles"
|
|
55
|
+
],
|
|
56
|
+
"properties": {
|
|
57
|
+
"maxIndexedFiles": { "type": "number" },
|
|
58
|
+
"maxIndexedBytes": { "type": "number" },
|
|
59
|
+
"maxSingleFileBytes": { "type": "number" },
|
|
60
|
+
"indexedFiles": { "type": "number" },
|
|
61
|
+
"indexedBytes": { "type": "number" },
|
|
62
|
+
"skippedFiles": { "type": "number" }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"cache": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"additionalProperties": true,
|
|
68
|
+
"required": ["used", "hit"],
|
|
69
|
+
"properties": {
|
|
70
|
+
"used": { "type": "boolean" },
|
|
71
|
+
"hit": { "type": "boolean" },
|
|
72
|
+
"manifestHash": { "type": "string" },
|
|
73
|
+
"createdAt": { "type": "string" },
|
|
74
|
+
"cachePath": { "type": "string" }
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"issues": { "type": "array", "items": { "$ref": "#/$defs/issue" } }
|
|
78
|
+
},
|
|
79
|
+
"$defs": {
|
|
80
|
+
"confidence": { "enum": ["explicit", "derived", "heuristic"] },
|
|
81
|
+
"fileKind": { "enum": ["source", "test", "fixture", "script", "config", "unknown"] },
|
|
82
|
+
"language": { "enum": ["typescript", "javascript", "json", "markdown", "unknown"] },
|
|
83
|
+
"symbolKind": { "enum": ["function", "class", "type", "interface", "const", "handler", "unknown"] },
|
|
84
|
+
"edgeType": {
|
|
85
|
+
"enum": [
|
|
86
|
+
"IMPORTS",
|
|
87
|
+
"EXPORTS",
|
|
88
|
+
"DEFINES_SYMBOL",
|
|
89
|
+
"TESTS_FILE",
|
|
90
|
+
"IMPLEMENTS_COMMAND",
|
|
91
|
+
"REFERENCED_BY_DOC",
|
|
92
|
+
"VALIDATED_BY_EVIDENCE"
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"source": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"additionalProperties": true,
|
|
98
|
+
"required": ["path", "extractor"],
|
|
99
|
+
"properties": {
|
|
100
|
+
"path": { "type": "string", "minLength": 1 },
|
|
101
|
+
"line": { "type": "number" },
|
|
102
|
+
"hash": { "type": "string" },
|
|
103
|
+
"extractor": { "type": "string", "minLength": 1 }
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"file": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": true,
|
|
109
|
+
"required": ["id", "path", "kind", "language", "hash", "lineCount", "exports", "imports", "commandFamilies"],
|
|
110
|
+
"properties": {
|
|
111
|
+
"id": { "type": "string", "minLength": 1 },
|
|
112
|
+
"path": { "type": "string", "minLength": 1 },
|
|
113
|
+
"kind": { "$ref": "#/$defs/fileKind" },
|
|
114
|
+
"language": { "$ref": "#/$defs/language" },
|
|
115
|
+
"hash": { "type": "string", "minLength": 1 },
|
|
116
|
+
"lineCount": { "type": "number" },
|
|
117
|
+
"exports": { "type": "array", "items": { "type": "string" } },
|
|
118
|
+
"imports": { "type": "array", "items": { "type": "string" } },
|
|
119
|
+
"commandFamilies": { "type": "array", "items": { "type": "string" } }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
"symbol": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"additionalProperties": true,
|
|
125
|
+
"required": ["id", "name", "kind", "path", "exported"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"id": { "type": "string", "minLength": 1 },
|
|
128
|
+
"name": { "type": "string", "minLength": 1 },
|
|
129
|
+
"kind": { "$ref": "#/$defs/symbolKind" },
|
|
130
|
+
"path": { "type": "string", "minLength": 1 },
|
|
131
|
+
"exported": { "type": "boolean" },
|
|
132
|
+
"line": { "type": "number" },
|
|
133
|
+
"endLine": { "type": "number" }
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"edge": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"additionalProperties": true,
|
|
139
|
+
"required": ["id", "from", "to", "type", "confidence", "reason", "source"],
|
|
140
|
+
"properties": {
|
|
141
|
+
"id": { "type": "string", "minLength": 1 },
|
|
142
|
+
"from": { "type": "string", "minLength": 1 },
|
|
143
|
+
"to": { "type": "string", "minLength": 1 },
|
|
144
|
+
"type": { "$ref": "#/$defs/edgeType" },
|
|
145
|
+
"confidence": { "$ref": "#/$defs/confidence" },
|
|
146
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
147
|
+
"source": { "$ref": "#/$defs/source" }
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"issue": {
|
|
151
|
+
"type": "object",
|
|
152
|
+
"additionalProperties": true,
|
|
153
|
+
"required": ["severity", "code", "message"],
|
|
154
|
+
"properties": {
|
|
155
|
+
"severity": { "enum": ["info", "warning", "error"] },
|
|
156
|
+
"code": {
|
|
157
|
+
"enum": [
|
|
158
|
+
"CODE_INDEX_FILE_READ_FAILED",
|
|
159
|
+
"CODE_INDEX_PARSE_DEGRADED",
|
|
160
|
+
"CODE_INDEX_TOO_LARGE",
|
|
161
|
+
"CODE_INDEX_UNSUPPORTED_LANGUAGE",
|
|
162
|
+
"CODE_INDEX_IMPORT_UNRESOLVED",
|
|
163
|
+
"CODE_INDEX_FILE_CACHE_CORRUPT",
|
|
164
|
+
"CODE_INDEX_FILE_CACHE_SCHEMA_MISMATCH"
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
"message": { "type": "string", "minLength": 1 },
|
|
168
|
+
"path": { "type": "string" },
|
|
169
|
+
"fixHint": { "type": "string" }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|