@codyswann/lisa 2.128.0 → 2.129.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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/doctor-report.mjs +103 -0
- package/plugins/lisa/scripts/plugin-sync-explain.mjs +64 -7
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/doctor-report.mjs +103 -0
- package/plugins/lisa-agy/scripts/plugin-sync-explain.mjs +64 -7
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/scripts/doctor-report.mjs +103 -0
- package/plugins/lisa-copilot/scripts/plugin-sync-explain.mjs +64 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/doctor-report.mjs +103 -0
- package/plugins/lisa-cursor/scripts/plugin-sync-explain.mjs +64 -7
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/scripts/doctor-report.mjs +103 -0
- package/plugins/src/base/scripts/plugin-sync-explain.mjs +64 -7
package/package.json
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"lodash": ">=4.18.1"
|
|
84
84
|
},
|
|
85
85
|
"name": "@codyswann/lisa",
|
|
86
|
-
"version": "2.
|
|
86
|
+
"version": "2.129.0",
|
|
87
87
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
88
88
|
"main": "dist/index.js",
|
|
89
89
|
"exports": {
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* repo adds real readiness probes. Keep this file dependency-free so future
|
|
7
7
|
* doctor scripts can reuse it from plugin distributions and downstream repos.
|
|
8
8
|
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
|
|
13
|
+
import { getPluginSyncResult } from "./plugin-sync-explain.mjs";
|
|
9
14
|
|
|
10
15
|
export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
|
|
11
16
|
export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
@@ -34,6 +39,90 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
|
34
39
|
* }} DoctorReportInput
|
|
35
40
|
*/
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} root
|
|
44
|
+
* @returns {DoctorGroup}
|
|
45
|
+
*/
|
|
46
|
+
export function createPluginSyncDoctorGroup(root = process.cwd()) {
|
|
47
|
+
const repoRoot = path.resolve(root);
|
|
48
|
+
if (
|
|
49
|
+
!existsSync(path.join(repoRoot, "plugins", "src")) &&
|
|
50
|
+
!existsSync(path.join(repoRoot, "plugins"))
|
|
51
|
+
) {
|
|
52
|
+
return {
|
|
53
|
+
id: "plugin-sync",
|
|
54
|
+
title: "Plugin source/generated sync",
|
|
55
|
+
checks: [
|
|
56
|
+
{
|
|
57
|
+
id: "plugin-sync",
|
|
58
|
+
status: "SKIP",
|
|
59
|
+
summary: "plugin sync check is not applicable",
|
|
60
|
+
observed:
|
|
61
|
+
"No plugins/ or plugins/src/ directory was found in this repository.",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = getPluginSyncResult(repoRoot);
|
|
69
|
+
if (!result.readOnly) {
|
|
70
|
+
return {
|
|
71
|
+
id: "plugin-sync",
|
|
72
|
+
title: "Plugin source/generated sync",
|
|
73
|
+
checks: [
|
|
74
|
+
{
|
|
75
|
+
id: "plugin-sync",
|
|
76
|
+
status: "FAIL",
|
|
77
|
+
summary: "plugin sync readiness check mutated the working tree",
|
|
78
|
+
observed:
|
|
79
|
+
"Git status changed while collecting plugin sync evidence.",
|
|
80
|
+
remediation:
|
|
81
|
+
"Run `git status --short`, inspect the unexpected changes, and fix plugin-sync-explain before trusting doctor output.",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: "plugin-sync",
|
|
89
|
+
title: "Plugin source/generated sync",
|
|
90
|
+
checks: [
|
|
91
|
+
{
|
|
92
|
+
id: "plugin-sync",
|
|
93
|
+
status: result.verdict,
|
|
94
|
+
summary:
|
|
95
|
+
result.verdict === "PASS"
|
|
96
|
+
? "plugin source and generated artifacts are in sync"
|
|
97
|
+
: `plugin sync drift detected: ${result.driftClass}`,
|
|
98
|
+
observed: renderPluginSyncObserved(result),
|
|
99
|
+
remediation:
|
|
100
|
+
result.remediations.length > 0
|
|
101
|
+
? result.remediations
|
|
102
|
+
.map(item => `${item.path}: ${item.nextAction}`)
|
|
103
|
+
.join(" ")
|
|
104
|
+
: undefined,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
id: "plugin-sync",
|
|
111
|
+
title: "Plugin source/generated sync",
|
|
112
|
+
checks: [
|
|
113
|
+
{
|
|
114
|
+
id: "plugin-sync",
|
|
115
|
+
status: "FAIL",
|
|
116
|
+
summary: "plugin sync readiness check failed",
|
|
117
|
+
observed: error instanceof Error ? error.message : String(error),
|
|
118
|
+
remediation:
|
|
119
|
+
"Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
37
126
|
/**
|
|
38
127
|
* @param {readonly DoctorGroup[]} groups
|
|
39
128
|
* @returns {DoctorVerdict}
|
|
@@ -141,3 +230,17 @@ function normalizeCheck(check) {
|
|
|
141
230
|
status: normalizedStatus,
|
|
142
231
|
};
|
|
143
232
|
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
function renderPluginSyncObserved(result) {
|
|
239
|
+
if (result.verdict === "PASS") {
|
|
240
|
+
return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
|
|
241
|
+
}
|
|
242
|
+
const paths = result.affectedPaths.length
|
|
243
|
+
? result.affectedPaths.join(", ")
|
|
244
|
+
: "none";
|
|
245
|
+
return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
|
|
246
|
+
}
|
|
@@ -50,6 +50,25 @@ const GIT_BIN = "/usr/bin/git";
|
|
|
50
50
|
* readonly readOnly: boolean
|
|
51
51
|
* readonly text: string
|
|
52
52
|
* }} PluginSyncReport
|
|
53
|
+
*
|
|
54
|
+
* @typedef {{
|
|
55
|
+
* readonly path: string
|
|
56
|
+
* readonly counterpart?: string
|
|
57
|
+
* readonly classification: string
|
|
58
|
+
* readonly nextAction: string
|
|
59
|
+
* }} PluginSyncRemediation
|
|
60
|
+
*
|
|
61
|
+
* @typedef {{
|
|
62
|
+
* readonly root: string
|
|
63
|
+
* readonly verdict: "PASS" | "WARN"
|
|
64
|
+
* readonly driftClass: string
|
|
65
|
+
* readonly affectedPaths: readonly string[]
|
|
66
|
+
* readonly remediations: readonly PluginSyncRemediation[]
|
|
67
|
+
* readonly findings: readonly PluginSyncFinding[]
|
|
68
|
+
* readonly statusBefore: string
|
|
69
|
+
* readonly statusAfter: string
|
|
70
|
+
* readonly readOnly: boolean
|
|
71
|
+
* }} PluginSyncResult
|
|
53
72
|
*/
|
|
54
73
|
|
|
55
74
|
/**
|
|
@@ -57,6 +76,25 @@ const GIT_BIN = "/usr/bin/git";
|
|
|
57
76
|
* @returns {PluginSyncReport}
|
|
58
77
|
*/
|
|
59
78
|
export function explainPluginSync(root = process.cwd()) {
|
|
79
|
+
const result = getPluginSyncResult(root);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
root: result.root,
|
|
83
|
+
findings: result.findings,
|
|
84
|
+
statusBefore: result.statusBefore,
|
|
85
|
+
statusAfter: result.statusAfter,
|
|
86
|
+
readOnly: result.readOnly,
|
|
87
|
+
text: renderPluginSyncReport(result),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Return a structured, read-only plugin sync result for readiness surfaces such
|
|
93
|
+
* as doctor. CLI callers should still render this through renderPluginSyncReport.
|
|
94
|
+
* @param {string} root
|
|
95
|
+
* @returns {PluginSyncResult}
|
|
96
|
+
*/
|
|
97
|
+
export function getPluginSyncResult(root = process.cwd()) {
|
|
60
98
|
const repoRoot = path.resolve(root);
|
|
61
99
|
const statusBefore = gitStatus(repoRoot);
|
|
62
100
|
const statusEntries = parseGitStatus(statusBefore);
|
|
@@ -71,20 +109,23 @@ export function explainPluginSync(root = process.cwd()) {
|
|
|
71
109
|
];
|
|
72
110
|
const statusAfter = gitStatus(repoRoot);
|
|
73
111
|
const readOnly = statusAfter === statusBefore;
|
|
112
|
+
const driftClass = highestClassification(findings);
|
|
74
113
|
|
|
75
114
|
return {
|
|
76
115
|
root: repoRoot,
|
|
116
|
+
verdict: findings.length === 0 ? "PASS" : "WARN",
|
|
117
|
+
driftClass,
|
|
118
|
+
affectedPaths: affectedPaths(findings),
|
|
119
|
+
remediations: findings.map(finding => ({
|
|
120
|
+
path: finding.path,
|
|
121
|
+
counterpart: finding.counterpart,
|
|
122
|
+
classification: finding.classification,
|
|
123
|
+
nextAction: finding.nextAction,
|
|
124
|
+
})),
|
|
77
125
|
findings,
|
|
78
126
|
statusBefore,
|
|
79
127
|
statusAfter,
|
|
80
128
|
readOnly,
|
|
81
|
-
text: renderPluginSyncReport({
|
|
82
|
-
root: repoRoot,
|
|
83
|
-
findings,
|
|
84
|
-
statusBefore,
|
|
85
|
-
statusAfter,
|
|
86
|
-
readOnly,
|
|
87
|
-
}),
|
|
88
129
|
};
|
|
89
130
|
}
|
|
90
131
|
|
|
@@ -142,6 +183,22 @@ function highestClassification(findings) {
|
|
|
142
183
|
return "IN_SYNC";
|
|
143
184
|
}
|
|
144
185
|
|
|
186
|
+
/**
|
|
187
|
+
* @param {readonly PluginSyncFinding[]} findings
|
|
188
|
+
* @returns {string[]}
|
|
189
|
+
*/
|
|
190
|
+
function affectedPaths(findings) {
|
|
191
|
+
return [
|
|
192
|
+
...new Set(
|
|
193
|
+
findings.flatMap(finding =>
|
|
194
|
+
[finding.path, finding.counterpart].filter(
|
|
195
|
+
pathValue => typeof pathValue === "string" && pathValue.length > 0
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
145
202
|
/**
|
|
146
203
|
* @param {readonly { readonly code: string, readonly file: string }[]} entries
|
|
147
204
|
* @param {ReadonlyMap<string, Buffer> | undefined} expectedGeneratedFiles
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* repo adds real readiness probes. Keep this file dependency-free so future
|
|
7
7
|
* doctor scripts can reuse it from plugin distributions and downstream repos.
|
|
8
8
|
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
|
|
13
|
+
import { getPluginSyncResult } from "./plugin-sync-explain.mjs";
|
|
9
14
|
|
|
10
15
|
export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
|
|
11
16
|
export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
@@ -34,6 +39,90 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
|
34
39
|
* }} DoctorReportInput
|
|
35
40
|
*/
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} root
|
|
44
|
+
* @returns {DoctorGroup}
|
|
45
|
+
*/
|
|
46
|
+
export function createPluginSyncDoctorGroup(root = process.cwd()) {
|
|
47
|
+
const repoRoot = path.resolve(root);
|
|
48
|
+
if (
|
|
49
|
+
!existsSync(path.join(repoRoot, "plugins", "src")) &&
|
|
50
|
+
!existsSync(path.join(repoRoot, "plugins"))
|
|
51
|
+
) {
|
|
52
|
+
return {
|
|
53
|
+
id: "plugin-sync",
|
|
54
|
+
title: "Plugin source/generated sync",
|
|
55
|
+
checks: [
|
|
56
|
+
{
|
|
57
|
+
id: "plugin-sync",
|
|
58
|
+
status: "SKIP",
|
|
59
|
+
summary: "plugin sync check is not applicable",
|
|
60
|
+
observed:
|
|
61
|
+
"No plugins/ or plugins/src/ directory was found in this repository.",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = getPluginSyncResult(repoRoot);
|
|
69
|
+
if (!result.readOnly) {
|
|
70
|
+
return {
|
|
71
|
+
id: "plugin-sync",
|
|
72
|
+
title: "Plugin source/generated sync",
|
|
73
|
+
checks: [
|
|
74
|
+
{
|
|
75
|
+
id: "plugin-sync",
|
|
76
|
+
status: "FAIL",
|
|
77
|
+
summary: "plugin sync readiness check mutated the working tree",
|
|
78
|
+
observed:
|
|
79
|
+
"Git status changed while collecting plugin sync evidence.",
|
|
80
|
+
remediation:
|
|
81
|
+
"Run `git status --short`, inspect the unexpected changes, and fix plugin-sync-explain before trusting doctor output.",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: "plugin-sync",
|
|
89
|
+
title: "Plugin source/generated sync",
|
|
90
|
+
checks: [
|
|
91
|
+
{
|
|
92
|
+
id: "plugin-sync",
|
|
93
|
+
status: result.verdict,
|
|
94
|
+
summary:
|
|
95
|
+
result.verdict === "PASS"
|
|
96
|
+
? "plugin source and generated artifacts are in sync"
|
|
97
|
+
: `plugin sync drift detected: ${result.driftClass}`,
|
|
98
|
+
observed: renderPluginSyncObserved(result),
|
|
99
|
+
remediation:
|
|
100
|
+
result.remediations.length > 0
|
|
101
|
+
? result.remediations
|
|
102
|
+
.map(item => `${item.path}: ${item.nextAction}`)
|
|
103
|
+
.join(" ")
|
|
104
|
+
: undefined,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
id: "plugin-sync",
|
|
111
|
+
title: "Plugin source/generated sync",
|
|
112
|
+
checks: [
|
|
113
|
+
{
|
|
114
|
+
id: "plugin-sync",
|
|
115
|
+
status: "FAIL",
|
|
116
|
+
summary: "plugin sync readiness check failed",
|
|
117
|
+
observed: error instanceof Error ? error.message : String(error),
|
|
118
|
+
remediation:
|
|
119
|
+
"Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
37
126
|
/**
|
|
38
127
|
* @param {readonly DoctorGroup[]} groups
|
|
39
128
|
* @returns {DoctorVerdict}
|
|
@@ -141,3 +230,17 @@ function normalizeCheck(check) {
|
|
|
141
230
|
status: normalizedStatus,
|
|
142
231
|
};
|
|
143
232
|
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
function renderPluginSyncObserved(result) {
|
|
239
|
+
if (result.verdict === "PASS") {
|
|
240
|
+
return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
|
|
241
|
+
}
|
|
242
|
+
const paths = result.affectedPaths.length
|
|
243
|
+
? result.affectedPaths.join(", ")
|
|
244
|
+
: "none";
|
|
245
|
+
return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
|
|
246
|
+
}
|
|
@@ -50,6 +50,25 @@ const GIT_BIN = "/usr/bin/git";
|
|
|
50
50
|
* readonly readOnly: boolean
|
|
51
51
|
* readonly text: string
|
|
52
52
|
* }} PluginSyncReport
|
|
53
|
+
*
|
|
54
|
+
* @typedef {{
|
|
55
|
+
* readonly path: string
|
|
56
|
+
* readonly counterpart?: string
|
|
57
|
+
* readonly classification: string
|
|
58
|
+
* readonly nextAction: string
|
|
59
|
+
* }} PluginSyncRemediation
|
|
60
|
+
*
|
|
61
|
+
* @typedef {{
|
|
62
|
+
* readonly root: string
|
|
63
|
+
* readonly verdict: "PASS" | "WARN"
|
|
64
|
+
* readonly driftClass: string
|
|
65
|
+
* readonly affectedPaths: readonly string[]
|
|
66
|
+
* readonly remediations: readonly PluginSyncRemediation[]
|
|
67
|
+
* readonly findings: readonly PluginSyncFinding[]
|
|
68
|
+
* readonly statusBefore: string
|
|
69
|
+
* readonly statusAfter: string
|
|
70
|
+
* readonly readOnly: boolean
|
|
71
|
+
* }} PluginSyncResult
|
|
53
72
|
*/
|
|
54
73
|
|
|
55
74
|
/**
|
|
@@ -57,6 +76,25 @@ const GIT_BIN = "/usr/bin/git";
|
|
|
57
76
|
* @returns {PluginSyncReport}
|
|
58
77
|
*/
|
|
59
78
|
export function explainPluginSync(root = process.cwd()) {
|
|
79
|
+
const result = getPluginSyncResult(root);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
root: result.root,
|
|
83
|
+
findings: result.findings,
|
|
84
|
+
statusBefore: result.statusBefore,
|
|
85
|
+
statusAfter: result.statusAfter,
|
|
86
|
+
readOnly: result.readOnly,
|
|
87
|
+
text: renderPluginSyncReport(result),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Return a structured, read-only plugin sync result for readiness surfaces such
|
|
93
|
+
* as doctor. CLI callers should still render this through renderPluginSyncReport.
|
|
94
|
+
* @param {string} root
|
|
95
|
+
* @returns {PluginSyncResult}
|
|
96
|
+
*/
|
|
97
|
+
export function getPluginSyncResult(root = process.cwd()) {
|
|
60
98
|
const repoRoot = path.resolve(root);
|
|
61
99
|
const statusBefore = gitStatus(repoRoot);
|
|
62
100
|
const statusEntries = parseGitStatus(statusBefore);
|
|
@@ -71,20 +109,23 @@ export function explainPluginSync(root = process.cwd()) {
|
|
|
71
109
|
];
|
|
72
110
|
const statusAfter = gitStatus(repoRoot);
|
|
73
111
|
const readOnly = statusAfter === statusBefore;
|
|
112
|
+
const driftClass = highestClassification(findings);
|
|
74
113
|
|
|
75
114
|
return {
|
|
76
115
|
root: repoRoot,
|
|
116
|
+
verdict: findings.length === 0 ? "PASS" : "WARN",
|
|
117
|
+
driftClass,
|
|
118
|
+
affectedPaths: affectedPaths(findings),
|
|
119
|
+
remediations: findings.map(finding => ({
|
|
120
|
+
path: finding.path,
|
|
121
|
+
counterpart: finding.counterpart,
|
|
122
|
+
classification: finding.classification,
|
|
123
|
+
nextAction: finding.nextAction,
|
|
124
|
+
})),
|
|
77
125
|
findings,
|
|
78
126
|
statusBefore,
|
|
79
127
|
statusAfter,
|
|
80
128
|
readOnly,
|
|
81
|
-
text: renderPluginSyncReport({
|
|
82
|
-
root: repoRoot,
|
|
83
|
-
findings,
|
|
84
|
-
statusBefore,
|
|
85
|
-
statusAfter,
|
|
86
|
-
readOnly,
|
|
87
|
-
}),
|
|
88
129
|
};
|
|
89
130
|
}
|
|
90
131
|
|
|
@@ -142,6 +183,22 @@ function highestClassification(findings) {
|
|
|
142
183
|
return "IN_SYNC";
|
|
143
184
|
}
|
|
144
185
|
|
|
186
|
+
/**
|
|
187
|
+
* @param {readonly PluginSyncFinding[]} findings
|
|
188
|
+
* @returns {string[]}
|
|
189
|
+
*/
|
|
190
|
+
function affectedPaths(findings) {
|
|
191
|
+
return [
|
|
192
|
+
...new Set(
|
|
193
|
+
findings.flatMap(finding =>
|
|
194
|
+
[finding.path, finding.counterpart].filter(
|
|
195
|
+
pathValue => typeof pathValue === "string" && pathValue.length > 0
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
),
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
145
202
|
/**
|
|
146
203
|
* @param {readonly { readonly code: string, readonly file: string }[]} entries
|
|
147
204
|
* @param {ReadonlyMap<string, Buffer> | undefined} expectedGeneratedFiles
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
* repo adds real readiness probes. Keep this file dependency-free so future
|
|
7
7
|
* doctor scripts can reuse it from plugin distributions and downstream repos.
|
|
8
8
|
*/
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import process from "node:process";
|
|
12
|
+
|
|
13
|
+
import { getPluginSyncResult } from "./plugin-sync-explain.mjs";
|
|
9
14
|
|
|
10
15
|
export const DOCTOR_STATUSES = ["PASS", "WARN", "FAIL", "SKIP"];
|
|
11
16
|
export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
@@ -34,6 +39,90 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
|
34
39
|
* }} DoctorReportInput
|
|
35
40
|
*/
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} root
|
|
44
|
+
* @returns {DoctorGroup}
|
|
45
|
+
*/
|
|
46
|
+
export function createPluginSyncDoctorGroup(root = process.cwd()) {
|
|
47
|
+
const repoRoot = path.resolve(root);
|
|
48
|
+
if (
|
|
49
|
+
!existsSync(path.join(repoRoot, "plugins", "src")) &&
|
|
50
|
+
!existsSync(path.join(repoRoot, "plugins"))
|
|
51
|
+
) {
|
|
52
|
+
return {
|
|
53
|
+
id: "plugin-sync",
|
|
54
|
+
title: "Plugin source/generated sync",
|
|
55
|
+
checks: [
|
|
56
|
+
{
|
|
57
|
+
id: "plugin-sync",
|
|
58
|
+
status: "SKIP",
|
|
59
|
+
summary: "plugin sync check is not applicable",
|
|
60
|
+
observed:
|
|
61
|
+
"No plugins/ or plugins/src/ directory was found in this repository.",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = getPluginSyncResult(repoRoot);
|
|
69
|
+
if (!result.readOnly) {
|
|
70
|
+
return {
|
|
71
|
+
id: "plugin-sync",
|
|
72
|
+
title: "Plugin source/generated sync",
|
|
73
|
+
checks: [
|
|
74
|
+
{
|
|
75
|
+
id: "plugin-sync",
|
|
76
|
+
status: "FAIL",
|
|
77
|
+
summary: "plugin sync readiness check mutated the working tree",
|
|
78
|
+
observed:
|
|
79
|
+
"Git status changed while collecting plugin sync evidence.",
|
|
80
|
+
remediation:
|
|
81
|
+
"Run `git status --short`, inspect the unexpected changes, and fix plugin-sync-explain before trusting doctor output.",
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: "plugin-sync",
|
|
89
|
+
title: "Plugin source/generated sync",
|
|
90
|
+
checks: [
|
|
91
|
+
{
|
|
92
|
+
id: "plugin-sync",
|
|
93
|
+
status: result.verdict,
|
|
94
|
+
summary:
|
|
95
|
+
result.verdict === "PASS"
|
|
96
|
+
? "plugin source and generated artifacts are in sync"
|
|
97
|
+
: `plugin sync drift detected: ${result.driftClass}`,
|
|
98
|
+
observed: renderPluginSyncObserved(result),
|
|
99
|
+
remediation:
|
|
100
|
+
result.remediations.length > 0
|
|
101
|
+
? result.remediations
|
|
102
|
+
.map(item => `${item.path}: ${item.nextAction}`)
|
|
103
|
+
.join(" ")
|
|
104
|
+
: undefined,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
id: "plugin-sync",
|
|
111
|
+
title: "Plugin source/generated sync",
|
|
112
|
+
checks: [
|
|
113
|
+
{
|
|
114
|
+
id: "plugin-sync",
|
|
115
|
+
status: "FAIL",
|
|
116
|
+
summary: "plugin sync readiness check failed",
|
|
117
|
+
observed: error instanceof Error ? error.message : String(error),
|
|
118
|
+
remediation:
|
|
119
|
+
"Run `/lisa:plugin-sync-explain` or `bun run check:plugins` to inspect plugin sync health directly.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
37
126
|
/**
|
|
38
127
|
* @param {readonly DoctorGroup[]} groups
|
|
39
128
|
* @returns {DoctorVerdict}
|
|
@@ -141,3 +230,17 @@ function normalizeCheck(check) {
|
|
|
141
230
|
status: normalizedStatus,
|
|
142
231
|
};
|
|
143
232
|
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {import("./plugin-sync-explain.mjs").PluginSyncResult} result
|
|
236
|
+
* @returns {string}
|
|
237
|
+
*/
|
|
238
|
+
function renderPluginSyncObserved(result) {
|
|
239
|
+
if (result.verdict === "PASS") {
|
|
240
|
+
return "Drift class IN_SYNC; plugin sync evidence was collected read-only.";
|
|
241
|
+
}
|
|
242
|
+
const paths = result.affectedPaths.length
|
|
243
|
+
? result.affectedPaths.join(", ")
|
|
244
|
+
: "none";
|
|
245
|
+
return `Drift class ${result.driftClass}; affected paths: ${paths}.`;
|
|
246
|
+
}
|