hive-lite 0.1.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 +443 -0
- package/bin/hive.js +6 -0
- package/docs/cli-semantics.md +386 -0
- package/docs/skills/hive-lite-finish/SKILL.md +282 -0
- package/docs/skills/hive-lite-finish/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-finish/references/safety.md +95 -0
- package/docs/skills/hive-lite-finish/references/verdicts.md +123 -0
- package/docs/skills/hive-lite-map-maintainer/SKILL.md +203 -0
- package/docs/skills/hive-lite-map-maintainer/agents/openai.yaml +7 -0
- package/docs/skills/hive-lite-map-maintainer/references/lifecycle.md +114 -0
- package/docs/skills/hive-lite-map-maintainer/references/repair-rules.md +201 -0
- package/docs/skills/hive-lite-start-prompt/SKILL.md +283 -0
- package/docs/skills/hive-lite-start-prompt/agents/openai.yaml +4 -0
- package/docs/skills/hive-lite-start-prompt/references/input-calibration.md +82 -0
- package/docs/skills/hive-lite-start-prompt/references/preflight.md +116 -0
- package/package.json +40 -0
- package/src/cli.js +910 -0
- package/src/lib/change.js +642 -0
- package/src/lib/context.js +1104 -0
- package/src/lib/evidence.js +230 -0
- package/src/lib/fsx.js +54 -0
- package/src/lib/git.js +128 -0
- package/src/lib/glob.js +47 -0
- package/src/lib/health.js +1012 -0
- package/src/lib/id.js +13 -0
- package/src/lib/map.js +713 -0
- package/src/lib/next.js +341 -0
- package/src/lib/risk.js +122 -0
- package/src/lib/roles.js +109 -0
- package/src/lib/scope.js +168 -0
- package/src/lib/skills.js +349 -0
- package/src/lib/status.js +344 -0
- package/src/lib/yaml.js +223 -0
package/src/lib/next.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
const { allDeltas, latestChangeId, loadChange } = require('./change');
|
|
2
|
+
const { repoRoot } = require('./git');
|
|
3
|
+
const { evaluateMapHealth } = require('./health');
|
|
4
|
+
const { skillPreflight } = require('./skills');
|
|
5
|
+
const { evaluateWorkspaceStatus } = require('./status');
|
|
6
|
+
|
|
7
|
+
function pendingDeltas(root) {
|
|
8
|
+
try {
|
|
9
|
+
return allDeltas(root).filter((delta) => delta.status !== 'applied' && delta.status !== 'rejected');
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function latestChangeSummary(root) {
|
|
16
|
+
const id = latestChangeId(root);
|
|
17
|
+
if (!id) return null;
|
|
18
|
+
try {
|
|
19
|
+
const change = loadChange(root, id);
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
verdict: change.evidencePolicy ? change.evidencePolicy.verdict : change.risk ? change.risk.verdict : null,
|
|
23
|
+
validationStatus: change.validation ? change.validation.status : null,
|
|
24
|
+
accepted: Boolean(change.humanDecision && change.humanDecision.status === 'accepted'),
|
|
25
|
+
committed: Boolean(change.humanDecision && change.humanDecision.commit),
|
|
26
|
+
decisionMode: change.humanDecision ? change.humanDecision.mode : null,
|
|
27
|
+
contextId: change.context ? change.context.id : null,
|
|
28
|
+
originSplit: change.originSplit || null,
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
verdict: null,
|
|
34
|
+
validationStatus: null,
|
|
35
|
+
accepted: false,
|
|
36
|
+
committed: false,
|
|
37
|
+
decisionMode: null,
|
|
38
|
+
contextId: null,
|
|
39
|
+
originSplit: null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mapHealthSummary(root) {
|
|
45
|
+
try {
|
|
46
|
+
const health = evaluateMapHealth(root, {});
|
|
47
|
+
return {
|
|
48
|
+
status: health.status,
|
|
49
|
+
critical: health.summary.critical,
|
|
50
|
+
warnings: health.summary.warnings,
|
|
51
|
+
info: health.summary.info,
|
|
52
|
+
areasChecked: health.summary.areasChecked,
|
|
53
|
+
topFindings: health.findings.slice(0, 5).map((finding) => ({
|
|
54
|
+
severity: finding.severity,
|
|
55
|
+
code: finding.code,
|
|
56
|
+
areaId: finding.areaId,
|
|
57
|
+
message: finding.message,
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
status: 'unknown',
|
|
63
|
+
critical: 0,
|
|
64
|
+
warnings: 0,
|
|
65
|
+
info: 0,
|
|
66
|
+
areasChecked: 0,
|
|
67
|
+
topFindings: [{
|
|
68
|
+
severity: 'warning',
|
|
69
|
+
code: 'MAP_HEALTH_UNAVAILABLE',
|
|
70
|
+
areaId: null,
|
|
71
|
+
message: error.message,
|
|
72
|
+
}],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function action(data) {
|
|
78
|
+
return {
|
|
79
|
+
kind: data.kind,
|
|
80
|
+
label: data.label,
|
|
81
|
+
command: data.command || null,
|
|
82
|
+
prompt: data.prompt || null,
|
|
83
|
+
requiresHuman: data.requiresHuman !== false,
|
|
84
|
+
enabled: data.enabled !== false,
|
|
85
|
+
disabledReason: data.disabledReason || null,
|
|
86
|
+
explanation: data.explanation || '',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startPrompt() {
|
|
91
|
+
return [
|
|
92
|
+
'$hive-lite-start-prompt',
|
|
93
|
+
'<describe the requirement you want to start>',
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function finishPrompt(changeId) {
|
|
98
|
+
return [
|
|
99
|
+
'$hive-lite-finish',
|
|
100
|
+
changeId ? `Close Hive Lite change ${changeId}.` : 'Close the current Hive Lite change.',
|
|
101
|
+
].join('\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function mapMaintainerPrompt(mode) {
|
|
105
|
+
if (mode === 'bootstrap') {
|
|
106
|
+
return [
|
|
107
|
+
'$hive-lite-map-maintainer',
|
|
108
|
+
'这个项目第一次使用 Hive Lite,请初始化并建立第一版 3-8 个高价值 areas。',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
if (mode === 'delta') {
|
|
112
|
+
return [
|
|
113
|
+
'$hive-lite-map-maintainer',
|
|
114
|
+
'请查看 map delta list,帮我判断哪些 delta 值得 apply,先不要自动 apply,等我确认。',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
return [
|
|
118
|
+
'$hive-lite-map-maintainer',
|
|
119
|
+
'已有 map,请做一次 refresh,按 map health 修复 stale paths、scope、roles、verification。',
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function splitStartPrompt(split) {
|
|
124
|
+
const phase = split.suggestedNextPhase;
|
|
125
|
+
return [
|
|
126
|
+
'$hive-lite-start-prompt',
|
|
127
|
+
`继续 split ${split.id}。`,
|
|
128
|
+
phase ? `建议下一 phase: ${phase.phaseId}` : '请先检查剩余 phase。',
|
|
129
|
+
phase && phase.findIntent ? `find intent: ${phase.findIntent}` : '',
|
|
130
|
+
].filter(Boolean).join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function chooseSplit(status) {
|
|
134
|
+
const splits = status.recentSplitNotes || [];
|
|
135
|
+
return splits.find((split) => split.suggestedNextPhase) || null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function requiredSkillForAction(actionItem) {
|
|
139
|
+
if (!actionItem) return null;
|
|
140
|
+
if (actionItem.kind === 'run_start_skill' || actionItem.kind === 'continue_split') return 'hive-lite-start-prompt';
|
|
141
|
+
if (actionItem.kind === 'run_finish_skill' || actionItem.kind === 'commit_accepted_change') return 'hive-lite-finish';
|
|
142
|
+
if (actionItem.kind === 'run_map_maintainer') return 'hive-lite-map-maintainer';
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applySkillPreflight(root, options, current) {
|
|
147
|
+
const requiredSkill = requiredSkillForAction(current.primaryAction);
|
|
148
|
+
const preflight = skillPreflight(root, requiredSkill, options);
|
|
149
|
+
if (!preflight) return { ...current, operatorSkillPreflight: null };
|
|
150
|
+
if (preflight.ready) return { ...current, operatorSkillPreflight: preflight };
|
|
151
|
+
|
|
152
|
+
const blockedAction = current.primaryAction;
|
|
153
|
+
return {
|
|
154
|
+
...current,
|
|
155
|
+
phaseGuess: 'skill_preflight',
|
|
156
|
+
primaryAction: action({
|
|
157
|
+
kind: 'install_operator_skill',
|
|
158
|
+
label: `Install ${preflight.requiredSkill}`,
|
|
159
|
+
command: preflight.installCommand,
|
|
160
|
+
explanation: 'The recommended next operator step requires this Hive Lite skill in the selected agent target.',
|
|
161
|
+
}),
|
|
162
|
+
secondaryActions: [
|
|
163
|
+
action({
|
|
164
|
+
...blockedAction,
|
|
165
|
+
enabled: false,
|
|
166
|
+
disabledReason: `Install ${preflight.requiredSkill} first for the selected agent target.`,
|
|
167
|
+
}),
|
|
168
|
+
...current.secondaryActions,
|
|
169
|
+
],
|
|
170
|
+
summaryForHuman: `需要先安装 ${preflight.requiredSkill},然后再继续 Hive Lite operator flow。`,
|
|
171
|
+
summaryForAgent: `The selected agent target is missing ${preflight.requiredSkill}. Run skills install before using the handoff skill.`,
|
|
172
|
+
operatorSkillPreflight: preflight,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function evaluateNextAction(cwd, options = {}) {
|
|
177
|
+
const root = repoRoot(cwd);
|
|
178
|
+
const workspace = evaluateWorkspaceStatus(root);
|
|
179
|
+
const repoSetupRequired = workspace.hive.state === 'repo_setup_required';
|
|
180
|
+
const mapHealth = repoSetupRequired
|
|
181
|
+
? { status: 'not_checked', critical: 0, warnings: 0, info: 0, areasChecked: 0, topFindings: [] }
|
|
182
|
+
: mapHealthSummary(root);
|
|
183
|
+
const latestChange = repoSetupRequired ? null : latestChangeSummary(root);
|
|
184
|
+
const deltas = repoSetupRequired ? [] : pendingDeltas(root);
|
|
185
|
+
const warnings = [];
|
|
186
|
+
const secondaryActions = [];
|
|
187
|
+
|
|
188
|
+
if (mapHealth.topFindings.length > 0) {
|
|
189
|
+
for (const finding of mapHealth.topFindings) {
|
|
190
|
+
if (finding.severity === 'critical' || finding.code === 'MAP_FILE_IGNORED') {
|
|
191
|
+
warnings.push(`${finding.code}: ${finding.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (deltas.length > 0) {
|
|
197
|
+
secondaryActions.push(action({
|
|
198
|
+
kind: 'review_map_deltas',
|
|
199
|
+
label: 'Review pending map deltas',
|
|
200
|
+
command: 'hive-lite map delta list',
|
|
201
|
+
prompt: mapMaintainerPrompt('delta'),
|
|
202
|
+
explanation: `${deltas.length} pending map delta candidate(s) are available for human review.`,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let phaseGuess = 'start';
|
|
207
|
+
let primaryAction = action({
|
|
208
|
+
kind: 'run_start_skill',
|
|
209
|
+
label: 'Use hive-lite-start-prompt',
|
|
210
|
+
prompt: startPrompt(),
|
|
211
|
+
explanation: 'The worktree is clean and Hive Lite is ready for a new bounded requirement.',
|
|
212
|
+
});
|
|
213
|
+
let summaryForHuman = '当前工作区干净,可以用 hive-lite-start-prompt 开始一个新的小需求。';
|
|
214
|
+
let summaryForAgent = 'Worktree is clean. Ask the human for the next requirement and use hive-lite-start-prompt.';
|
|
215
|
+
|
|
216
|
+
if (workspace.hive.state === 'repo_setup_required') {
|
|
217
|
+
phaseGuess = 'repo_setup_required';
|
|
218
|
+
primaryAction = action({
|
|
219
|
+
kind: 'repo_setup_required',
|
|
220
|
+
label: 'Stop: git repository required',
|
|
221
|
+
explanation: 'Hive Lite needs git before it can safely run find/check/accept. Switch to the correct repo root, or manually initialize git and create an initial commit before using Hive Lite.',
|
|
222
|
+
});
|
|
223
|
+
summaryForHuman = '当前目录不是 git repo。Hive Lite 不能安全开始;请切到正确的 repo 根目录,或你自己先完成 git 初始化和初始提交。';
|
|
224
|
+
summaryForAgent = 'No git repository detected. Stop. Do not run find/check/accept or initialize git automatically.';
|
|
225
|
+
} else if (!workspace.canStartNewRequirement) {
|
|
226
|
+
if (workspace.hive.state === 'in_progress') {
|
|
227
|
+
phaseGuess = 'finish';
|
|
228
|
+
primaryAction = action({
|
|
229
|
+
kind: 'run_finish_skill',
|
|
230
|
+
label: 'Use hive-lite-finish',
|
|
231
|
+
prompt: finishPrompt(workspace.hive.latestChange && workspace.hive.latestChange.id),
|
|
232
|
+
explanation: 'There is an in-progress Hive Change Record that should be checked, validated, or accepted before starting new work.',
|
|
233
|
+
});
|
|
234
|
+
summaryForHuman = '当前有一个 Hive change 还在进行中,建议先用 hive-lite-finish 收尾。';
|
|
235
|
+
summaryForAgent = 'There is an in-progress Hive change. Do not start a new requirement; use hive-lite-finish.';
|
|
236
|
+
} else if (workspace.hive.state === 'accepted_uncommitted') {
|
|
237
|
+
phaseGuess = 'finish';
|
|
238
|
+
primaryAction = action({
|
|
239
|
+
kind: 'commit_accepted_change',
|
|
240
|
+
label: 'Commit accepted Hive change',
|
|
241
|
+
command: workspace.hive.latestChange ? `hive-lite accept ${workspace.hive.latestChange.id} --commit -m "<message>"` : null,
|
|
242
|
+
prompt: finishPrompt(workspace.hive.latestChange && workspace.hive.latestChange.id),
|
|
243
|
+
explanation: 'The current dirty diff matches an accepted Hive change that still needs a git commit boundary.',
|
|
244
|
+
});
|
|
245
|
+
summaryForHuman = '当前 change 已经 accept,但还没有 commit。建议先用 finish/accept 创建提交边界。';
|
|
246
|
+
summaryForAgent = 'The latest Hive change is accepted but uncommitted. Ask the human before committing.';
|
|
247
|
+
} else {
|
|
248
|
+
phaseGuess = 'preflight';
|
|
249
|
+
primaryAction = action({
|
|
250
|
+
kind: 'resolve_dirty_worktree',
|
|
251
|
+
label: 'Resolve unmanaged dirty worktree',
|
|
252
|
+
command: 'hive-lite status',
|
|
253
|
+
explanation: 'The worktree has unmanaged dirty changes. Commit, stash, use a separate worktree, or stop before starting a Hive Lite requirement.',
|
|
254
|
+
});
|
|
255
|
+
summaryForHuman = '当前工作区有 unmanaged dirty diff,不建议开始新需求。请先 commit、stash、另开 worktree,或停止。';
|
|
256
|
+
summaryForAgent = 'Unmanaged dirty worktree. Do not run find for a new requirement until the human chooses how to isolate the changes.';
|
|
257
|
+
}
|
|
258
|
+
} else if (mapHealth.status === 'invalid_map' || mapHealth.critical > 0) {
|
|
259
|
+
phaseGuess = 'map';
|
|
260
|
+
primaryAction = action({
|
|
261
|
+
kind: 'run_map_maintainer',
|
|
262
|
+
label: 'Use hive-lite-map-maintainer',
|
|
263
|
+
prompt: mapMaintainerPrompt(mapHealth.areasChecked === 0 ? 'bootstrap' : 'refresh'),
|
|
264
|
+
explanation: 'Project Map health has critical findings, so find/check may not produce safe context or evidence policy.',
|
|
265
|
+
});
|
|
266
|
+
summaryForHuman = 'Project Map 目前有 critical 问题,建议先用 hive-lite-map-maintainer 修 map。';
|
|
267
|
+
summaryForAgent = 'Project Map has critical health findings. Do not start coding work; run map maintainer flow.';
|
|
268
|
+
} else if (mapHealth.status === 'needs_attention') {
|
|
269
|
+
phaseGuess = 'map';
|
|
270
|
+
primaryAction = action({
|
|
271
|
+
kind: 'run_map_maintainer',
|
|
272
|
+
label: 'Use hive-lite-map-maintainer',
|
|
273
|
+
prompt: mapMaintainerPrompt('refresh'),
|
|
274
|
+
explanation: 'Project Map is usable but has warnings that may reduce find/check quality.',
|
|
275
|
+
});
|
|
276
|
+
summaryForHuman = 'Project Map 可用但有 warning。建议先 refresh map,或者在明确知道风险时继续 start。';
|
|
277
|
+
summaryForAgent = 'Project Map needs attention. Prefer map maintainer before new work, unless the human chooses to proceed.';
|
|
278
|
+
secondaryActions.unshift(action({
|
|
279
|
+
kind: 'run_start_skill',
|
|
280
|
+
label: 'Start anyway with caution',
|
|
281
|
+
prompt: startPrompt(),
|
|
282
|
+
explanation: 'Use only if the warnings are not relevant to the next requirement.',
|
|
283
|
+
}));
|
|
284
|
+
} else {
|
|
285
|
+
const split = chooseSplit(workspace);
|
|
286
|
+
if (split) {
|
|
287
|
+
phaseGuess = 'split_continue';
|
|
288
|
+
primaryAction = action({
|
|
289
|
+
kind: 'continue_split',
|
|
290
|
+
label: 'Continue recent split note',
|
|
291
|
+
prompt: splitStartPrompt(split),
|
|
292
|
+
explanation: 'A recent large intent was decomposed and has a ready remaining phase.',
|
|
293
|
+
});
|
|
294
|
+
summaryForHuman = `发现未完成的 split note:${split.id}。建议继续 ready phase,而不是重新开一个大需求。`;
|
|
295
|
+
summaryForAgent = 'A recent split note has a ready phase. Ask the human whether to continue it via hive-lite-start-prompt.';
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const withSkillPreflight = applySkillPreflight(root, options, {
|
|
300
|
+
phaseGuess,
|
|
301
|
+
summaryForHuman,
|
|
302
|
+
summaryForAgent,
|
|
303
|
+
primaryAction,
|
|
304
|
+
secondaryActions,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
version: 1,
|
|
309
|
+
command: 'next',
|
|
310
|
+
generatedAt: new Date().toISOString(),
|
|
311
|
+
root,
|
|
312
|
+
phaseGuess: withSkillPreflight.phaseGuess,
|
|
313
|
+
summaryForHuman: withSkillPreflight.summaryForHuman,
|
|
314
|
+
summaryForAgent: withSkillPreflight.summaryForAgent,
|
|
315
|
+
primaryAction: withSkillPreflight.primaryAction,
|
|
316
|
+
secondaryActions: withSkillPreflight.secondaryActions,
|
|
317
|
+
warnings,
|
|
318
|
+
operatorSkillPreflight: withSkillPreflight.operatorSkillPreflight,
|
|
319
|
+
state: {
|
|
320
|
+
branch: workspace.branch,
|
|
321
|
+
head: workspace.head,
|
|
322
|
+
worktree: workspace.worktree,
|
|
323
|
+
hive: workspace.hive,
|
|
324
|
+
canStartNewRequirement: workspace.canStartNewRequirement,
|
|
325
|
+
latestChange,
|
|
326
|
+
mapHealth,
|
|
327
|
+
pendingMapDeltas: deltas.map((delta) => ({
|
|
328
|
+
id: delta.id,
|
|
329
|
+
type: delta.type,
|
|
330
|
+
status: delta.status,
|
|
331
|
+
target: delta.target || null,
|
|
332
|
+
})),
|
|
333
|
+
recentSplitNotes: workspace.recentSplitNotes || [],
|
|
334
|
+
splitNoteSummary: workspace.splitNoteSummary || null,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
evaluateNextAction,
|
|
341
|
+
};
|
package/src/lib/risk.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const { firstPatternMatch } = require('./glob');
|
|
2
|
+
|
|
3
|
+
function normalizePath(value) {
|
|
4
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\//, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function validationSurfaceWeakened(diff) {
|
|
8
|
+
if (!diff || !diff.trim()) return false;
|
|
9
|
+
const removedValidationCommand = /^-[^-].*\b(test|tests|lint|typecheck|tsc|vitest|jest|eslint|prettier|coverage|check|checks)\b/im.test(diff);
|
|
10
|
+
const addedBypass = /^\+[^+].*(continue-on-error:\s*true|allow_failure:\s*true|\|\|\s*true|&&\s*true\b|exit\s+0\b|--no-verify\b|--passWithNoTests\b|skip[-_ ]?(tests?|checks?|validation)|disable[-_ ]?(tests?|checks?|validation))/im.test(diff);
|
|
11
|
+
return removedValidationCommand || addedBypass;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function defaultSensitiveReason(file) {
|
|
15
|
+
const normalized = normalizePath(file);
|
|
16
|
+
const patterns = [
|
|
17
|
+
[/(\b|\/)(auth|oauth|session|sessions|permission|permissions|rbac|acl)(\/|\.|$)/i, 'auth_or_permissions'],
|
|
18
|
+
[/(\b|\/)(payment|payments|billing|stripe|invoice|invoices)(\/|\.|$)/i, 'payment_or_billing'],
|
|
19
|
+
[/(\b|\/)(security|secrets?|credentials?|pii|privacy|encrypt|encryption|crypto)(\/|\.|$)/i, 'security_or_privacy'],
|
|
20
|
+
[/(\b|\/)(migrations?|drizzle)(\/|$)|(^|\/)schema\.sql$/i, 'db_or_migration'],
|
|
21
|
+
[/(^|\/)(\.github\/workflows|\.gitlab-ci\.yml|Dockerfile|docker-compose|deploy|deployment|infra|k8s|helm)(\/|\.|$)/i, 'ci_cd_or_deploy'],
|
|
22
|
+
[/(^|\/)(package\.json|bun\.lock|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$/i, 'dependency_surface'],
|
|
23
|
+
];
|
|
24
|
+
const hit = patterns.find(([regex]) => regex.test(normalized));
|
|
25
|
+
return hit ? hit[1] : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function configuredMatches(files, policies) {
|
|
29
|
+
return files.map((file) => ({ file, policy: firstPatternMatch(file, policies || []) })).filter((item) => item.policy);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validationStatus(change) {
|
|
33
|
+
const results = (change.validation && change.validation.results) || [];
|
|
34
|
+
if (results.some((result) => result.status === 'failed' || result.exitCode > 0)) return 'failed';
|
|
35
|
+
if (results.length > 0 && results.every((result) => result.status === 'passed' || result.exitCode === 0)) return 'passed';
|
|
36
|
+
return change.validation ? change.validation.status || 'not_run' : 'not_run';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasRequiredValidation(change) {
|
|
40
|
+
const plan = change.validation && change.validation.plan ? change.validation.plan : [];
|
|
41
|
+
return plan.some((item) => item.required !== false && item.type !== 'manual' && item.command);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function evaluateChangeRisk(change, map) {
|
|
45
|
+
const files = (change.diff.changedFiles || []).map((item) => item.path || item);
|
|
46
|
+
const diff = change.diff.text || '';
|
|
47
|
+
const blockingReasons = [];
|
|
48
|
+
const reviewReasons = [];
|
|
49
|
+
const signals = [];
|
|
50
|
+
|
|
51
|
+
if (!change.repo.baselineCommit) blockingReasons.push('missing baseline commit');
|
|
52
|
+
if (files.length > 0 && !diff.trim()) blockingReasons.push('diff could not be captured for changed files');
|
|
53
|
+
|
|
54
|
+
if (change.scope.status === 'violation') {
|
|
55
|
+
blockingReasons.push(`scope violation: ${change.scope.violations.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const status = validationStatus(change);
|
|
59
|
+
if (status === 'failed') blockingReasons.push('validation failed');
|
|
60
|
+
|
|
61
|
+
if (validationSurfaceWeakened(diff)) {
|
|
62
|
+
blockingReasons.push('diff appears to weaken validation, CI, or deploy checks');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const configuredSensitive = configuredMatches(files, map.rules.sensitive_paths || []);
|
|
66
|
+
for (const match of configuredSensitive) {
|
|
67
|
+
reviewReasons.push(`sensitive path (${match.policy.reason || match.policy.pattern}): ${match.file}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const codeOwner = configuredMatches(files, map.rules.code_owner_paths || []);
|
|
71
|
+
for (const match of codeOwner) {
|
|
72
|
+
const owners = match.policy.owners ? ` -> ${match.policy.owners.join(', ')}` : '';
|
|
73
|
+
reviewReasons.push(`code-owner path${owners}: ${match.file}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const security = configuredMatches(files, map.rules.security_review_paths || []);
|
|
77
|
+
for (const match of security) {
|
|
78
|
+
reviewReasons.push(`security-review path: ${match.file}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const reason = defaultSensitiveReason(file);
|
|
83
|
+
if (reason) reviewReasons.push(`${reason}: ${file}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (change.scope.status === 'unknown' || change.scope.status === 'needs_review') {
|
|
87
|
+
reviewReasons.push(`scope ${change.scope.status}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const areaIds = new Set((change.scope.matchedAreas || []).filter(Boolean));
|
|
91
|
+
if (areaIds.size > 1) reviewReasons.push('change spans multiple map areas');
|
|
92
|
+
if (files.length > 8) reviewReasons.push(`changed file count ${files.length} exceeds light-change threshold`);
|
|
93
|
+
|
|
94
|
+
if (files.length === 0) signals.push('no changed files');
|
|
95
|
+
else if (files.length === 1) signals.push('single file change');
|
|
96
|
+
else signals.push(`${files.length} files changed`);
|
|
97
|
+
|
|
98
|
+
const requiresValidation = hasRequiredValidation(change);
|
|
99
|
+
let verdict = 'acceptable';
|
|
100
|
+
if (blockingReasons.length > 0) verdict = 'blocked';
|
|
101
|
+
else if (requiresValidation && status === 'not_run') verdict = 'needs_validation';
|
|
102
|
+
else if (reviewReasons.length > 0) verdict = 'needs_review';
|
|
103
|
+
|
|
104
|
+
const level = blockingReasons.length > 0 ? 'high'
|
|
105
|
+
: reviewReasons.length > 0 ? 'medium'
|
|
106
|
+
: files.length <= 2 ? 'low'
|
|
107
|
+
: 'medium';
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
verdict,
|
|
111
|
+
level,
|
|
112
|
+
blockingReasons: [...new Set(blockingReasons)],
|
|
113
|
+
reviewReasons: [...new Set(reviewReasons)],
|
|
114
|
+
signals: [...new Set(signals)],
|
|
115
|
+
evaluatedAt: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
evaluateChangeRisk,
|
|
121
|
+
validationStatus,
|
|
122
|
+
};
|
package/src/lib/roles.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const ROLE_TAXONOMY = [
|
|
2
|
+
'page',
|
|
3
|
+
'ui_component',
|
|
4
|
+
'styles',
|
|
5
|
+
'state_store',
|
|
6
|
+
'grouping_logic',
|
|
7
|
+
'data_transform',
|
|
8
|
+
'api_contract',
|
|
9
|
+
'schema_logic',
|
|
10
|
+
'cli_behavior',
|
|
11
|
+
'error_handling',
|
|
12
|
+
'cache_logic',
|
|
13
|
+
'config',
|
|
14
|
+
'docs',
|
|
15
|
+
'test',
|
|
16
|
+
'unknown',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const ROLE_SET = new Set(ROLE_TAXONOMY);
|
|
20
|
+
|
|
21
|
+
const ROLE_ALIASES = new Map([
|
|
22
|
+
['main_component', 'ui_component'],
|
|
23
|
+
['item_component', 'ui_component'],
|
|
24
|
+
['component', 'ui_component'],
|
|
25
|
+
['view', 'ui_component'],
|
|
26
|
+
['ui', 'ui_component'],
|
|
27
|
+
['main_page', 'page'],
|
|
28
|
+
['copy', 'docs'],
|
|
29
|
+
['parser', 'data_transform'],
|
|
30
|
+
['formatter', 'data_transform'],
|
|
31
|
+
['code', 'unknown'],
|
|
32
|
+
['file', 'unknown'],
|
|
33
|
+
['entrypoint', 'unknown'],
|
|
34
|
+
['grep_hit', 'unknown'],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const UI_MANUAL_ROLES = new Set(['page', 'ui_component', 'styles', 'docs']);
|
|
38
|
+
const INTERNAL_BEHAVIOR_ROLES = new Set([
|
|
39
|
+
'state_store',
|
|
40
|
+
'grouping_logic',
|
|
41
|
+
'data_transform',
|
|
42
|
+
'api_contract',
|
|
43
|
+
'schema_logic',
|
|
44
|
+
'cli_behavior',
|
|
45
|
+
'error_handling',
|
|
46
|
+
'cache_logic',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
function normalizeRole(role) {
|
|
50
|
+
const raw = String(role || '').trim();
|
|
51
|
+
if (!raw) return '';
|
|
52
|
+
if (ROLE_SET.has(raw)) return raw;
|
|
53
|
+
return ROLE_ALIASES.get(raw) || '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isKnownRole(role) {
|
|
57
|
+
return Boolean(normalizeRole(role));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isCanonicalRole(role) {
|
|
61
|
+
return ROLE_SET.has(String(role || '').trim());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isUiManualRole(role) {
|
|
65
|
+
return UI_MANUAL_ROLES.has(normalizeRole(role));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isInternalBehaviorRole(role) {
|
|
69
|
+
return INTERNAL_BEHAVIOR_ROLES.has(normalizeRole(role));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inferRoleFromPath(filePath) {
|
|
73
|
+
const file = String(filePath || '').replace(/\\/g, '/').toLowerCase();
|
|
74
|
+
const base = file.split('/').pop() || '';
|
|
75
|
+
|
|
76
|
+
if (/(^|\/)(__tests__|tests?|specs?)\//.test(file) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(base)) return 'test';
|
|
77
|
+
if (/\.(css|scss|sass|less)$/.test(base)) return 'styles';
|
|
78
|
+
if (/\.(md|mdx|txt)$/.test(base) || /(^|\/)docs?\//.test(file) || /(^|\/)readme(\.|$)/.test(file)) return 'docs';
|
|
79
|
+
if (/\.github\/workflows\//.test(file) || /(^|\/)config\//.test(file) || /\.config\./.test(base) || /(^|\/)package\.json$/.test(file) || /lock$|lock\.yaml$|lock\.json$/.test(base)) return 'config';
|
|
80
|
+
if (/(^|\/)(cli|commands|bin)\//.test(file)) return 'cli_behavior';
|
|
81
|
+
if (/(^|\/)(stores?|state)\//.test(file) || /store\.[cm]?[jt]sx?$/.test(base)) return 'state_store';
|
|
82
|
+
if (/(schema|zod|contract|contracts|types?)/.test(file)) {
|
|
83
|
+
return /(api|contract|contracts)/.test(file) ? 'api_contract' : 'schema_logic';
|
|
84
|
+
}
|
|
85
|
+
if (/(group|grouping|sort|filter|partition)/.test(base)) return 'grouping_logic';
|
|
86
|
+
if (/(transform|normalize|mapper|serializer|deserialize|format)/.test(base)) return 'data_transform';
|
|
87
|
+
if (/(error|retry|exception|fallback|recover)/.test(base)) return 'error_handling';
|
|
88
|
+
if (/(cache|memo|storage)/.test(base)) return 'cache_logic';
|
|
89
|
+
if (/(^|\/)(pages?|views?|routes?)\//.test(file) || /page\.[cm]?[jt]sx?$/.test(base)) return 'page';
|
|
90
|
+
if (/(^|\/)components?\//.test(file) || /\.(vue|svelte|tsx|jsx)$/.test(base)) return 'ui_component';
|
|
91
|
+
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function roleListText() {
|
|
96
|
+
return ROLE_TAXONOMY.join(', ');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
ROLE_TAXONOMY,
|
|
101
|
+
ROLE_SET,
|
|
102
|
+
normalizeRole,
|
|
103
|
+
isKnownRole,
|
|
104
|
+
isCanonicalRole,
|
|
105
|
+
isUiManualRole,
|
|
106
|
+
isInternalBehaviorRole,
|
|
107
|
+
inferRoleFromPath,
|
|
108
|
+
roleListText,
|
|
109
|
+
};
|