@codyswann/lisa 2.279.2 → 2.281.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/dist/cli/doctor-readiness-audit-allowlist.d.ts +7 -0
- package/dist/cli/doctor-readiness-audit-allowlist.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-audit-allowlist.js +177 -0
- package/dist/cli/doctor-readiness-audit-allowlist.js.map +1 -0
- package/dist/cli/doctor-readiness-capabilities.d.ts +33 -0
- package/dist/cli/doctor-readiness-capabilities.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-capabilities.js +26 -0
- package/dist/cli/doctor-readiness-capabilities.js.map +1 -0
- package/dist/cli/doctor-readiness-context-claims.d.ts +14 -0
- package/dist/cli/doctor-readiness-context-claims.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-context-claims.js +117 -0
- package/dist/cli/doctor-readiness-context-claims.js.map +1 -0
- package/dist/cli/doctor-readiness-context-mechanisms.d.ts +46 -0
- package/dist/cli/doctor-readiness-context-mechanisms.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-context-mechanisms.js +116 -0
- package/dist/cli/doctor-readiness-context-mechanisms.js.map +1 -0
- package/dist/cli/doctor-readiness-context.d.ts +16 -0
- package/dist/cli/doctor-readiness-context.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-context.js +244 -0
- package/dist/cli/doctor-readiness-context.js.map +1 -0
- package/dist/cli/doctor-readiness-delivery.d.ts.map +1 -1
- package/dist/cli/doctor-readiness-delivery.js +1 -10
- package/dist/cli/doctor-readiness-delivery.js.map +1 -1
- package/dist/cli/doctor-readiness-domain.d.ts +17 -0
- package/dist/cli/doctor-readiness-domain.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-domain.js +197 -0
- package/dist/cli/doctor-readiness-domain.js.map +1 -0
- package/dist/cli/doctor-readiness-guardrails.d.ts +15 -0
- package/dist/cli/doctor-readiness-guardrails.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-guardrails.js +219 -0
- package/dist/cli/doctor-readiness-guardrails.js.map +1 -0
- package/dist/cli/doctor-readiness-operations.d.ts +103 -0
- package/dist/cli/doctor-readiness-operations.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-operations.js +207 -0
- package/dist/cli/doctor-readiness-operations.js.map +1 -0
- package/dist/cli/doctor-readiness-shared.d.ts +47 -0
- package/dist/cli/doctor-readiness-shared.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-shared.js +88 -0
- package/dist/cli/doctor-readiness-shared.js.map +1 -0
- package/dist/cli/doctor-readiness-supply-chain-scan.d.ts +67 -0
- package/dist/cli/doctor-readiness-supply-chain-scan.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-supply-chain-scan.js +316 -0
- package/dist/cli/doctor-readiness-supply-chain-scan.js.map +1 -0
- package/dist/cli/doctor-readiness-supply-chain.d.ts +14 -0
- package/dist/cli/doctor-readiness-supply-chain.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-supply-chain.js +231 -0
- package/dist/cli/doctor-readiness-supply-chain.js.map +1 -0
- package/dist/cli/doctor-readiness-workflows.d.ts +20 -0
- package/dist/cli/doctor-readiness-workflows.d.ts.map +1 -1
- package/dist/cli/doctor-readiness-workflows.js +29 -7
- package/dist/cli/doctor-readiness-workflows.js.map +1 -1
- package/dist/cli/doctor-readiness-workspaces.d.ts +22 -0
- package/dist/cli/doctor-readiness-workspaces.d.ts.map +1 -0
- package/dist/cli/doctor-readiness-workspaces.js +98 -0
- package/dist/cli/doctor-readiness-workspaces.js.map +1 -0
- package/dist/cli/doctor-readiness.d.ts.map +1 -1
- package/dist/cli/doctor-readiness.js +23 -13
- package/dist/cli/doctor-readiness.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +23 -0
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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-agy/plugin.json +1 -1
- 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-cursor/.claude-plugin/plugin.json +1 -1
- 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-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The feedback/guardrails readiness producer — ship blocker B4 (PRD #1739,
|
|
3
|
+
* #1896).
|
|
4
|
+
*
|
|
5
|
+
* Dimension 5 of the `readiness-rubric` asks whether a failing loop produces a
|
|
6
|
+
* named outcome and a runbook. B4 asks the operational half: does every
|
|
7
|
+
* irreversible or expensive operation have a gate AND a way back?
|
|
8
|
+
*
|
|
9
|
+
* Only part of that is answerable offline, and saying so plainly is the point.
|
|
10
|
+
* Whether a declared GitHub `environment:` actually carries protection rules —
|
|
11
|
+
* required reviewers, wait timers, branch restrictions — lives in the GitHub API,
|
|
12
|
+
* not in any file this producer can read. That half is therefore a **stated
|
|
13
|
+
* SKIP in every record emitted here**, never a guess in either direction.
|
|
14
|
+
*
|
|
15
|
+
* What remains is a narrow, file-provable subset: a job that runs an
|
|
16
|
+
* irreversible or expensive operation with **no `environment:` key at all**, no
|
|
17
|
+
* `if:` gate, reachable from an automated trigger, in a repository that checks
|
|
18
|
+
* in no runbook. Only that stands B4. Everything else — an ordinary `cdk
|
|
19
|
+
* deploy`, a CI-database migration, a job that names any environment, a reusable
|
|
20
|
+
* workflow whose caller is invisible — is an observation at most, because a
|
|
21
|
+
* standing blocker flips the whole repository to `NOT_READY` and `WARN` does not
|
|
22
|
+
* soften that.
|
|
23
|
+
* @module cli/doctor-readiness-guardrails
|
|
24
|
+
*/
|
|
25
|
+
import { describeCommands, hasCheckedInRunbook, looksEphemeral, scanCommands, } from "./doctor-readiness-operations.js";
|
|
26
|
+
import { informationalFindings } from "./doctor-readiness-shared.js";
|
|
27
|
+
import { parseRepositoryWorkflows, } from "./doctor-readiness-workflows.js";
|
|
28
|
+
/** The feedback/guardrails readiness dimension id (readiness-rubric, RRR-1). */
|
|
29
|
+
export const FEEDBACK_GUARDRAILS_DIMENSION_ID = "feedback-guardrails";
|
|
30
|
+
/** The ship blocker for a consequential operation with no gate and no recovery. */
|
|
31
|
+
const GUARDRAILS_BLOCKER_ID = "B4";
|
|
32
|
+
/** Most operations quoted in one finding before summarizing the remainder. */
|
|
33
|
+
const MAX_EVIDENCE_COMMANDS = 10;
|
|
34
|
+
/**
|
|
35
|
+
* The half of B4 no offline read can settle, stated in every record this
|
|
36
|
+
* producer emits so an operator is never left to infer that a declared
|
|
37
|
+
* environment was checked for protection rules. It was not.
|
|
38
|
+
*/
|
|
39
|
+
const PROTECTION_RULES_SKIP_REASON = "Whether a declared `environment:` actually gates anything — required " +
|
|
40
|
+
"reviewers, wait timers, branch restrictions — is a GitHub environment " +
|
|
41
|
+
"protection rule, which lives in the GitHub API and is not resolvable " +
|
|
42
|
+
"offline. That half of this question was deliberately not assessed; only the " +
|
|
43
|
+
"file-provable half (an irreversible or expensive operation with no " +
|
|
44
|
+
"`environment:` key at all and no checked-in runbook) was.";
|
|
45
|
+
/**
|
|
46
|
+
* Operations that are irreversible or expensive enough that running them
|
|
47
|
+
* unattended is a decision, not a detail. `cdk deploy` and a plain `terraform
|
|
48
|
+
* apply` are absent on purpose: they are how normal delivery works, and a
|
|
49
|
+
* blocker that fires on normal delivery is a blocker nobody will believe.
|
|
50
|
+
*/
|
|
51
|
+
const CONSEQUENTIAL_OPS = [
|
|
52
|
+
/\bterraform\s+(\S+\s+)*apply\b[^\n]*-auto-approve\b/,
|
|
53
|
+
/\bterraform\s+(\S+\s+)*destroy\b/,
|
|
54
|
+
/\bcdk\s+destroy\b/,
|
|
55
|
+
/\bpulumi\s+destroy\b/,
|
|
56
|
+
/\bpulumi\s+up\b[^\n]*--yes\b/,
|
|
57
|
+
/\bserverless\s+remove\b/,
|
|
58
|
+
];
|
|
59
|
+
/**
|
|
60
|
+
* Commands that apply schema migrations, in the ecosystems Lisa templates. The
|
|
61
|
+
* `(?!:)` lookahead keeps the read-only subcommands out: `db:migrate:status`
|
|
62
|
+
* and `db:migrate:version` report what has run, they do not run anything.
|
|
63
|
+
*/
|
|
64
|
+
const MIGRATION_COMMANDS = [
|
|
65
|
+
/\bdb:migrate\b(?!:)/,
|
|
66
|
+
/\bmigrate\s+deploy\b/,
|
|
67
|
+
/\bmigration:run\b/,
|
|
68
|
+
/\balembic\s+upgrade\b/,
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Markers that a command explicitly targets production. A migration is only
|
|
72
|
+
* consequential when the file itself says it runs against production: the same
|
|
73
|
+
* command against a CI service container is ordinary test setup, and treating
|
|
74
|
+
* the two alike is the false positive that would discredit this whole check.
|
|
75
|
+
*/
|
|
76
|
+
const PRODUCTION_MARKERS = [
|
|
77
|
+
/\b(rails_env|node_env|app_env|django_settings_module|environment)=[^\s]*prod/,
|
|
78
|
+
/--env(ironment)?[= ]prod/,
|
|
79
|
+
/\bprod(uction)?\.tfvars\b/,
|
|
80
|
+
];
|
|
81
|
+
/**
|
|
82
|
+
* Whether a command is an irreversible or expensive operation.
|
|
83
|
+
*
|
|
84
|
+
* The ephemeral screen is the same one B1 uses, widened to the per-pull-request
|
|
85
|
+
* markers this blocker meets constantly: tearing down `pr-1234` or a
|
|
86
|
+
* `PreviewStack` destroys what the same pipeline built minutes earlier, and
|
|
87
|
+
* there is nothing durable there for a gate to protect. The job's `env:` block
|
|
88
|
+
* is read alongside the command, since the stage name usually lives there.
|
|
89
|
+
* @param command - The lower-cased `run` command
|
|
90
|
+
* @param job - The job the command runs in
|
|
91
|
+
* @returns True when the command is consequential on its face
|
|
92
|
+
*/
|
|
93
|
+
function isConsequential(command, job) {
|
|
94
|
+
if (looksEphemeral(`${command}\n${job.env}`)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return (CONSEQUENTIAL_OPS.some(pattern => pattern.test(command)) ||
|
|
98
|
+
(MIGRATION_COMMANDS.some(pattern => pattern.test(command)) &&
|
|
99
|
+
PRODUCTION_MARKERS.some(pattern => pattern.test(command))));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Triggers that fire on the lifecycle of a short-lived resource rather than on
|
|
103
|
+
* a release. A workflow that runs on a closing pull request or a deleted branch
|
|
104
|
+
* is tearing down what that branch created.
|
|
105
|
+
*/
|
|
106
|
+
const EPHEMERAL_LIFECYCLE_EVENTS = [
|
|
107
|
+
"pull_request",
|
|
108
|
+
"pull_request_target",
|
|
109
|
+
"delete",
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* State why a workflow driven by a branch or pull-request lifecycle cannot be
|
|
113
|
+
* settled offline. This is deferred rather than cleared: the operation is real,
|
|
114
|
+
* but whether it touches anything durable is not provable from the file.
|
|
115
|
+
* @param workflow - The workflow to consider
|
|
116
|
+
* @returns A stated reason, or null when the trigger is a release-style one
|
|
117
|
+
*/
|
|
118
|
+
function lifecycleDeferral(workflow) {
|
|
119
|
+
const events = workflow.on.events.filter(event => EPHEMERAL_LIFECYCLE_EVENTS.includes(event));
|
|
120
|
+
if (events.length === 0) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return (`\`${workflow.file}\` runs a consequential operation on the \`${events.join(", ")}\` ` +
|
|
124
|
+
"lifecycle, which tears down what that same branch or pull request created, " +
|
|
125
|
+
"so whether it ever touches a durable environment is not established " +
|
|
126
|
+
"either way — no gate was expected and none was required");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Build the rubric-shaped B4 finding from the operations it cites.
|
|
130
|
+
* @param scan - The consequential-operation scan
|
|
131
|
+
* @returns The B4 finding
|
|
132
|
+
*/
|
|
133
|
+
function guardrailsFinding(scan) {
|
|
134
|
+
return {
|
|
135
|
+
blocker: GUARDRAILS_BLOCKER_ID,
|
|
136
|
+
invariant_violated: "every irreversible or expensive operation has a gate before it and a " +
|
|
137
|
+
"way back after it",
|
|
138
|
+
evidence: describeCommands(scan.ungated, MAX_EVIDENCE_COMMANDS),
|
|
139
|
+
why_proof_missed: "the job declares no `environment:` at all, so there is nothing for a " +
|
|
140
|
+
"protection rule to attach to and nothing for an operator to approve — " +
|
|
141
|
+
"and with no runbook checked in, the first time anyone works out how to " +
|
|
142
|
+
"undo the operation is while it is already failing",
|
|
143
|
+
root_correction: "declare an `environment:` on the job so a protection rule can gate it, " +
|
|
144
|
+
"and check in a runbook naming the recovery procedure and the run " +
|
|
145
|
+
"outcome the loop reports, per the `automation-runbook-contract` rule",
|
|
146
|
+
machinery_to_remove: [
|
|
147
|
+
"auto-approval flags that exist only because no approval gate was ever " +
|
|
148
|
+
"configured for the environment the job deploys to",
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build the SKIP record. Its first finding always states the network-bound half
|
|
154
|
+
* that was not assessed; the observations below it carry whatever consequential
|
|
155
|
+
* operations were found to be already gated.
|
|
156
|
+
* @param scan - The consequential-operation scan
|
|
157
|
+
* @param inspected - How many workflow files were read
|
|
158
|
+
* @param runbook - Whether a runbook is checked in
|
|
159
|
+
* @returns The SKIP dimension record
|
|
160
|
+
*/
|
|
161
|
+
function skipRecord(scan, inspected, runbook) {
|
|
162
|
+
return {
|
|
163
|
+
id: FEEDBACK_GUARDRAILS_DIMENSION_ID,
|
|
164
|
+
status: "SKIP",
|
|
165
|
+
findings: [
|
|
166
|
+
{
|
|
167
|
+
reason: `Read ${inspected} workflow file(s) for irreversible or expensive ` +
|
|
168
|
+
"operations and found none that this file alone proves runs " +
|
|
169
|
+
`unattended with no gate and no way back (recovery runbook ` +
|
|
170
|
+
`${runbook ? "present" : "absent"}). ${PROTECTION_RULES_SKIP_REASON}`,
|
|
171
|
+
skip: true,
|
|
172
|
+
},
|
|
173
|
+
...informationalFindings([
|
|
174
|
+
...scan.gated.map(entry => `\`${entry.workflow}\` job \`${entry.jobId}\` runs a consequential ` +
|
|
175
|
+
`operation (\`${entry.command}\`) behind a declared environment, an ` +
|
|
176
|
+
"`if:` condition, its own confirmation switch, or in a repository " +
|
|
177
|
+
"that checks in a recovery runbook, so it was not treated as " +
|
|
178
|
+
"ungated and unrecoverable — whether any such gate carries " +
|
|
179
|
+
"protection rules was not checked"),
|
|
180
|
+
...scan.unresolved,
|
|
181
|
+
]),
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Assess the feedback/guardrails dimension: B4, "a consequential operation has
|
|
187
|
+
* no gate and no recovery". Offline by construction, and honest about it — the
|
|
188
|
+
* environment-protection-rule half is reasoned-SKIPped in every record, so this
|
|
189
|
+
* dimension never reports `PASS`. B4 stands only on the file-provable subset.
|
|
190
|
+
* @param root - Project root to assess
|
|
191
|
+
* @param parsedWorkflows - Pre-parsed workflows (default: parse `root`)
|
|
192
|
+
* @returns The feedback/guardrails dimension record
|
|
193
|
+
*/
|
|
194
|
+
export async function assessFeedbackGuardrailsDimension(root, parsedWorkflows) {
|
|
195
|
+
const workflows = parsedWorkflows ?? (await parseRepositoryWorkflows(root));
|
|
196
|
+
const scan = scanCommands(workflows, isConsequential, {
|
|
197
|
+
unresolvedWorkflow: lifecycleDeferral,
|
|
198
|
+
});
|
|
199
|
+
const runbook = await hasCheckedInRunbook(root);
|
|
200
|
+
if (scan.ungated.length === 0 || runbook) {
|
|
201
|
+
return skipRecord(runbook ? { ...scan, gated: [...scan.gated, ...scan.ungated] } : scan, workflows.length, runbook);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
id: FEEDBACK_GUARDRAILS_DIMENSION_ID,
|
|
205
|
+
// `WARN` reports how much confidence an offline read can carry; it does NOT
|
|
206
|
+
// soften the verdict. The blocker engine never reads this status.
|
|
207
|
+
status: "WARN",
|
|
208
|
+
findings: [
|
|
209
|
+
guardrailsFinding(scan),
|
|
210
|
+
// The unassessable half must travel with the standing blocker too:
|
|
211
|
+
// dropping it here would let a reader believe the gate was checked.
|
|
212
|
+
...informationalFindings([
|
|
213
|
+
PROTECTION_RULES_SKIP_REASON,
|
|
214
|
+
...scan.unresolved,
|
|
215
|
+
]),
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=doctor-readiness-guardrails.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-guardrails.js","sourceRoot":"","sources":["../../src/cli/doctor-readiness-guardrails.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { ParsedWorkflow, ParsedWorkflowJob, ParsedWorkflowStep } from "./doctor-readiness-workflows.js";
|
|
2
|
+
/** One command an automated job runs, with the file coordinates to cite it. */
|
|
3
|
+
export interface ScannedCommand {
|
|
4
|
+
readonly workflow: string;
|
|
5
|
+
readonly jobId: string;
|
|
6
|
+
readonly command: string;
|
|
7
|
+
}
|
|
8
|
+
/** What one scan of a repository's workflows established. */
|
|
9
|
+
export interface OperationScan {
|
|
10
|
+
/** Commands running in a job this file proves is automated and ungated. */
|
|
11
|
+
readonly ungated: readonly ScannedCommand[];
|
|
12
|
+
/** Commands running in a job something already gates. */
|
|
13
|
+
readonly gated: readonly ScannedCommand[];
|
|
14
|
+
/** Stated reasons a job's gating could not be settled offline. */
|
|
15
|
+
readonly unresolved: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/** Optional refinements a caller layers onto a scan. */
|
|
18
|
+
export interface ScanOptions {
|
|
19
|
+
/**
|
|
20
|
+
* A job-level exemption, evaluated when a match is found. B1 uses it for a
|
|
21
|
+
* job that takes its own backup first: the destructive command is real, but
|
|
22
|
+
* the way back is right there in the same job.
|
|
23
|
+
*/
|
|
24
|
+
readonly exemptJob?: (job: ParsedWorkflowJob) => boolean;
|
|
25
|
+
/**
|
|
26
|
+
* A workflow-level reason the match cannot be settled offline, returning
|
|
27
|
+
* `null` when it can. Used for triggers that make the operation obviously
|
|
28
|
+
* ephemeral — a per-pull-request teardown destroys what that same pull
|
|
29
|
+
* request created, so there is nothing durable for a gate to protect.
|
|
30
|
+
*/
|
|
31
|
+
readonly unresolvedWorkflow?: (workflow: ParsedWorkflow) => string | null;
|
|
32
|
+
/**
|
|
33
|
+
* A job-level reason the match cannot be settled offline, returning `null`
|
|
34
|
+
* when it can. Used for a job that boots its own `services:` containers.
|
|
35
|
+
*/
|
|
36
|
+
readonly unresolvedJob?: (job: ParsedWorkflowJob) => string | null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Whether any text — a command, a job's `env:` block, or the two together —
|
|
40
|
+
* names an obviously ephemeral target.
|
|
41
|
+
* @param text - The text to screen (case is normalized here)
|
|
42
|
+
* @returns True when the text names throwaway state
|
|
43
|
+
*/
|
|
44
|
+
export declare function looksEphemeral(text: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Whether one command is the kind of operation a producer is looking for. The
|
|
47
|
+
* job travels with the command because the evidence that settles the question
|
|
48
|
+
* is often outside the command text — a job `env:` block naming a CI database,
|
|
49
|
+
* for instance.
|
|
50
|
+
*/
|
|
51
|
+
export type CommandPredicate = (command: string, job: ParsedWorkflowJob) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Whether a workflow's declared triggers prove it runs without a human. A
|
|
54
|
+
* workflow that declares no `on:` block this parser could read is treated as
|
|
55
|
+
* NOT automated, because absence of a readable trigger is absence of proof.
|
|
56
|
+
* @param workflow - The parsed workflow
|
|
57
|
+
* @returns True when at least one trigger fires with no human in the loop
|
|
58
|
+
*/
|
|
59
|
+
export declare function runsUnattended(workflow: ParsedWorkflow): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Whether anything in the file already gates this job. A declared
|
|
62
|
+
* `environment:` counts even though its protection rules are unreadable
|
|
63
|
+
* offline: the operator has named a deployment boundary, and calling that
|
|
64
|
+
* ungated would be exactly the false positive this module exists to prevent.
|
|
65
|
+
* @param job - The parsed job
|
|
66
|
+
* @returns True when the job is conditional or names an environment
|
|
67
|
+
*/
|
|
68
|
+
export declare function jobIsGated(job: ParsedWorkflowJob): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a step is individually gated — a conditional step, or a command that
|
|
71
|
+
* names its own confirmation or rehearsal switch.
|
|
72
|
+
* @param step - The parsed step
|
|
73
|
+
* @returns True when the step carries its own guard
|
|
74
|
+
*/
|
|
75
|
+
export declare function stepIsGated(step: ParsedWorkflowStep): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Scan every workflow for the commands matching a predicate, partitioned by
|
|
78
|
+
* whether the file itself proves the command runs unattended and ungated.
|
|
79
|
+
* @param workflows - Parsed workflows
|
|
80
|
+
* @param matches - Predicate over a lower-cased `run` command
|
|
81
|
+
* @param options - Scan refinements
|
|
82
|
+
* @returns The partitioned scan
|
|
83
|
+
*/
|
|
84
|
+
export declare function scanCommands(workflows: readonly ParsedWorkflow[], matches: CommandPredicate, options?: ScanOptions): OperationScan;
|
|
85
|
+
/**
|
|
86
|
+
* Whether the repository checks in an automation runbook — the "way back" half
|
|
87
|
+
* of B1 and B4. The `automation-runbook-contract` rule puts them under
|
|
88
|
+
* `.lisa/automations/*.runbook.md`; the conventional documentation homes are
|
|
89
|
+
* honored as well, so a project that writes its recovery procedure somewhere
|
|
90
|
+
* reasonable is never told it has none.
|
|
91
|
+
* @param root - Repository root
|
|
92
|
+
* @returns True when at least one runbook is checked in
|
|
93
|
+
*/
|
|
94
|
+
export declare function hasCheckedInRunbook(root: string): Promise<boolean>;
|
|
95
|
+
/**
|
|
96
|
+
* Render scanned commands as one evidence string, quoting the file, the job, and
|
|
97
|
+
* the command so the operator can go straight to the line.
|
|
98
|
+
* @param commands - The commands to cite
|
|
99
|
+
* @param limit - Most commands to quote before summarizing the remainder
|
|
100
|
+
* @returns One evidence string
|
|
101
|
+
*/
|
|
102
|
+
export declare function describeCommands(commands: readonly ScannedCommand[], limit: number): string;
|
|
103
|
+
//# sourceMappingURL=doctor-readiness-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-operations.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-operations.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,iCAAiC,CAAC;AAEzC,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,6DAA6D;AAC7D,MAAM,WAAW,aAAa;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,yDAAyD;IACzD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wDAAwD;AACxD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC;IACzD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,MAAM,GAAG,IAAI,CAAC;IAC1E;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,GAAG,IAAI,CAAC;CACpE;AAsBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,iBAAiB,KACnB,OAAO,CAAC;AAiBb;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAMhE;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAE1D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAa7D;AAwDD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,WAAgB,GACxB,aAAa,CAef;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAWxE;AAsBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,SAAS,cAAc,EAAE,EACnC,KAAK,EAAE,MAAM,GACZ,MAAM,CAaR"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared consequential-operation scanning for the B1 and B4 readiness producers
|
|
3
|
+
* (PRD #1739, #1896).
|
|
4
|
+
*
|
|
5
|
+
* B1 ("a realistic path causes silent data loss") and B4 ("a consequential
|
|
6
|
+
* operation has no gate and no recovery") ask different questions about the same
|
|
7
|
+
* raw material: a command an automated pipeline runs. They therefore share one
|
|
8
|
+
* definition of what counts as *automated* and what counts as *gated*, so the
|
|
9
|
+
* two producers cannot drift into disagreeing about the same workflow file.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is written to be wrong in the safe direction. These are the
|
|
12
|
+
* two most false-positive-prone blockers in the rubric, and a standing blocker
|
|
13
|
+
* flips the entire repository to `NOT_READY` no matter what status the finding
|
|
14
|
+
* carries. So a guard this module cannot see is treated as a guard that exists:
|
|
15
|
+
* a conditional step, a conditional job, a declared environment, a manual-only
|
|
16
|
+
* trigger, or a reusable workflow whose caller is invisible all mean "not
|
|
17
|
+
* provable from this file", and not-provable never stands a blocker.
|
|
18
|
+
* @module cli/doctor-readiness-operations
|
|
19
|
+
*/
|
|
20
|
+
import { readdir } from "node:fs/promises";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
/**
|
|
23
|
+
* Targets that mark an operation as aimed at throwaway state — a CI database,
|
|
24
|
+
* a per-pull-request preview stack, a scratch stage. A pipeline that tears down
|
|
25
|
+
* what it just built is doing the correct thing, and reporting it as an ungated
|
|
26
|
+
* consequential operation is the archetypal false positive for both B1 and B4.
|
|
27
|
+
* Shared by both producers so they cannot drift apart on what "ephemeral" means.
|
|
28
|
+
*/
|
|
29
|
+
const EPHEMERAL_TARGETS = [
|
|
30
|
+
/(^|[^a-z0-9])test([^a-z0-9]|$)/,
|
|
31
|
+
/_test\b/,
|
|
32
|
+
/-test\b/,
|
|
33
|
+
/\blocalhost\b/,
|
|
34
|
+
/\b127\.0\.0\.1\b/,
|
|
35
|
+
/(^|[^a-z0-9])(tmp|temp)([^a-z0-9]|$)/,
|
|
36
|
+
/(^|[^a-z0-9])pr-/,
|
|
37
|
+
/\bpreview\b/,
|
|
38
|
+
/github\.event\.number/,
|
|
39
|
+
/\bpull_request\b/,
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Whether any text — a command, a job's `env:` block, or the two together —
|
|
43
|
+
* names an obviously ephemeral target.
|
|
44
|
+
* @param text - The text to screen (case is normalized here)
|
|
45
|
+
* @returns True when the text names throwaway state
|
|
46
|
+
*/
|
|
47
|
+
export function looksEphemeral(text) {
|
|
48
|
+
const normalized = text.toLowerCase();
|
|
49
|
+
return EPHEMERAL_TARGETS.some(pattern => pattern.test(normalized));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Triggers that mean the workflow does not run itself. `workflow_dispatch` puts
|
|
53
|
+
* a human at the console, and `workflow_call` hides the caller from an offline
|
|
54
|
+
* read — neither is a path this module may call automated.
|
|
55
|
+
*/
|
|
56
|
+
const NON_AUTOMATED_EVENTS = [
|
|
57
|
+
"workflow_dispatch",
|
|
58
|
+
"workflow_call",
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* Whether a workflow's declared triggers prove it runs without a human. A
|
|
62
|
+
* workflow that declares no `on:` block this parser could read is treated as
|
|
63
|
+
* NOT automated, because absence of a readable trigger is absence of proof.
|
|
64
|
+
* @param workflow - The parsed workflow
|
|
65
|
+
* @returns True when at least one trigger fires with no human in the loop
|
|
66
|
+
*/
|
|
67
|
+
export function runsUnattended(workflow) {
|
|
68
|
+
// An empty trigger list yields `false` here, which is the intended reading:
|
|
69
|
+
// a workflow whose `on:` block could not be parsed proves nothing.
|
|
70
|
+
return workflow.on.events.some(event => !NON_AUTOMATED_EVENTS.includes(event));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether anything in the file already gates this job. A declared
|
|
74
|
+
* `environment:` counts even though its protection rules are unreadable
|
|
75
|
+
* offline: the operator has named a deployment boundary, and calling that
|
|
76
|
+
* ungated would be exactly the false positive this module exists to prevent.
|
|
77
|
+
* @param job - The parsed job
|
|
78
|
+
* @returns True when the job is conditional or names an environment
|
|
79
|
+
*/
|
|
80
|
+
export function jobIsGated(job) {
|
|
81
|
+
return job.ifCondition !== "" || job.environment.length > 0;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether a step is individually gated — a conditional step, or a command that
|
|
85
|
+
* names its own confirmation or rehearsal switch.
|
|
86
|
+
* @param step - The parsed step
|
|
87
|
+
* @returns True when the step carries its own guard
|
|
88
|
+
*/
|
|
89
|
+
export function stepIsGated(step) {
|
|
90
|
+
if (step.ifCondition !== "") {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
const command = step.run.toLowerCase();
|
|
94
|
+
return (
|
|
95
|
+
// `-dryrun` covers the AWS CLI's own hyphen-less spelling and the
|
|
96
|
+
// double-dashed one; `--dry-run` is the GNU-style form everything else uses.
|
|
97
|
+
command.includes("--dry-run") ||
|
|
98
|
+
command.includes("-dryrun") ||
|
|
99
|
+
command.includes("--require-approval") ||
|
|
100
|
+
command.includes("inputs.confirm"));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* State the reason one job's exposure could not be settled from the file alone.
|
|
104
|
+
* @param workflow - The workflow declaring the job
|
|
105
|
+
* @param job - The job that matched
|
|
106
|
+
* @returns One operator-language reason line
|
|
107
|
+
*/
|
|
108
|
+
function unresolvedReason(workflow, job) {
|
|
109
|
+
return (`\`${workflow.file}\` job \`${job.id}\` runs a consequential operation, but ` +
|
|
110
|
+
"the workflow only starts from a manual dispatch or from a caller this " +
|
|
111
|
+
"offline read cannot see, so whether it ever runs unattended is not " +
|
|
112
|
+
"established either way");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Classify every matching step in one job.
|
|
116
|
+
* @param workflow - The workflow declaring the job
|
|
117
|
+
* @param job - The job to classify
|
|
118
|
+
* @param matches - Predicate over a lower-cased `run` command
|
|
119
|
+
* @param options - Scan refinements
|
|
120
|
+
* @returns The classified outcomes for this job (empty when nothing matched)
|
|
121
|
+
*/
|
|
122
|
+
function classifyJob(workflow, job, matches, options) {
|
|
123
|
+
const hits = job.steps.filter(step => step.run !== "" && matches(step.run.toLowerCase(), job));
|
|
124
|
+
if (hits.length === 0) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
if (!runsUnattended(workflow)) {
|
|
128
|
+
return [{ kind: "unresolved", reason: unresolvedReason(workflow, job) }];
|
|
129
|
+
}
|
|
130
|
+
const deferred = options.unresolvedWorkflow?.(workflow) ?? options.unresolvedJob?.(job);
|
|
131
|
+
if (deferred !== undefined && deferred !== null) {
|
|
132
|
+
return [{ kind: "unresolved", reason: deferred }];
|
|
133
|
+
}
|
|
134
|
+
const jobGated = jobIsGated(job) || (options.exemptJob?.(job) ?? false);
|
|
135
|
+
return hits.map(step => ({
|
|
136
|
+
kind: jobGated || stepIsGated(step) ? "gated" : "ungated",
|
|
137
|
+
command: { workflow: workflow.file, jobId: job.id, command: step.run },
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Scan every workflow for the commands matching a predicate, partitioned by
|
|
142
|
+
* whether the file itself proves the command runs unattended and ungated.
|
|
143
|
+
* @param workflows - Parsed workflows
|
|
144
|
+
* @param matches - Predicate over a lower-cased `run` command
|
|
145
|
+
* @param options - Scan refinements
|
|
146
|
+
* @returns The partitioned scan
|
|
147
|
+
*/
|
|
148
|
+
export function scanCommands(workflows, matches, options = {}) {
|
|
149
|
+
const outcomes = workflows.flatMap(workflow => workflow.jobs.flatMap(job => classifyJob(workflow, job, matches, options)));
|
|
150
|
+
return {
|
|
151
|
+
ungated: outcomes.flatMap(outcome => outcome.kind === "ungated" ? [outcome.command] : []),
|
|
152
|
+
gated: outcomes.flatMap(outcome => outcome.kind === "gated" ? [outcome.command] : []),
|
|
153
|
+
unresolved: outcomes.flatMap(outcome => outcome.kind === "unresolved" ? [outcome.reason] : []),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether the repository checks in an automation runbook — the "way back" half
|
|
158
|
+
* of B1 and B4. The `automation-runbook-contract` rule puts them under
|
|
159
|
+
* `.lisa/automations/*.runbook.md`; the conventional documentation homes are
|
|
160
|
+
* honored as well, so a project that writes its recovery procedure somewhere
|
|
161
|
+
* reasonable is never told it has none.
|
|
162
|
+
* @param root - Repository root
|
|
163
|
+
* @returns True when at least one runbook is checked in
|
|
164
|
+
*/
|
|
165
|
+
export async function hasCheckedInRunbook(root) {
|
|
166
|
+
const holdsRunbooks = await Promise.all([
|
|
167
|
+
directoryHolds(root, path.join(".lisa", "automations"), ".runbook.md"),
|
|
168
|
+
...["wiki/runbooks", "docs/runbooks", "runbooks"].map(async (candidate) =>
|
|
169
|
+
// An EMPTY directory is not a recovery procedure. Accepting one would
|
|
170
|
+
// make `mkdir runbooks` a one-command switch for turning B1 and B4 off.
|
|
171
|
+
await directoryHolds(root, candidate, ".md")),
|
|
172
|
+
]);
|
|
173
|
+
return holdsRunbooks.includes(true);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether a repository directory holds at least one file with a given suffix.
|
|
177
|
+
* @param root - Repository root
|
|
178
|
+
* @param relativePath - Repo-relative directory path
|
|
179
|
+
* @param suffix - Required file-name suffix
|
|
180
|
+
* @returns True when the directory exists and holds a matching file
|
|
181
|
+
*/
|
|
182
|
+
async function directoryHolds(root, relativePath, suffix) {
|
|
183
|
+
try {
|
|
184
|
+
const entries = await readdir(path.join(root, ...relativePath.split("/")));
|
|
185
|
+
return entries.some(entry => entry.endsWith(suffix));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Render scanned commands as one evidence string, quoting the file, the job, and
|
|
193
|
+
* the command so the operator can go straight to the line.
|
|
194
|
+
* @param commands - The commands to cite
|
|
195
|
+
* @param limit - Most commands to quote before summarizing the remainder
|
|
196
|
+
* @returns One evidence string
|
|
197
|
+
*/
|
|
198
|
+
export function describeCommands(commands, limit) {
|
|
199
|
+
const shown = commands.slice(0, limit);
|
|
200
|
+
const overflow = commands.length - shown.length;
|
|
201
|
+
const lines = shown.map(entry => `\`${entry.workflow}\` job \`${entry.jobId}\` runs \`${entry.command}\``);
|
|
202
|
+
return (lines.join(" | ") +
|
|
203
|
+
(overflow > 0
|
|
204
|
+
? ` | (+${overflow} further operation(s) of the same kind)`
|
|
205
|
+
: ""));
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=doctor-readiness-operations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-operations.js","sourceRoot":"","sources":["../../src/cli/doctor-readiness-operations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a repository-relative file, returning null when it cannot be read.
|
|
3
|
+
*
|
|
4
|
+
* Reading is best-effort by design: a file that cannot be read establishes
|
|
5
|
+
* nothing, and a readiness producer must never turn "could not look" into
|
|
6
|
+
* either a violation or a clean bill of health.
|
|
7
|
+
* @param root - Repository root
|
|
8
|
+
* @param relativePath - Repo-relative path (forward slashes)
|
|
9
|
+
* @returns File contents, or null
|
|
10
|
+
*/
|
|
11
|
+
export declare function readFileOrNull(root: string, relativePath: string): Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Whether a repository-relative directory exists.
|
|
14
|
+
* @param root - Repository root
|
|
15
|
+
* @param relativePath - Repo-relative directory path
|
|
16
|
+
* @returns True when the directory could be listed
|
|
17
|
+
*/
|
|
18
|
+
export declare function directoryExists(root: string, relativePath: string): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether a repository-relative path exists, as a file or a directory.
|
|
21
|
+
* @param root - Repository root
|
|
22
|
+
* @param relativePath - Repo-relative path
|
|
23
|
+
* @returns True when something is present at that path
|
|
24
|
+
*/
|
|
25
|
+
export declare function pathExists(root: string, relativePath: string): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Strip leading and trailing slashes from a repo-relative path fragment.
|
|
28
|
+
* Written without a regex quantifier so it cannot backtrack on hostile input.
|
|
29
|
+
* @param value - The path fragment
|
|
30
|
+
* @returns The fragment with empty segments removed
|
|
31
|
+
*/
|
|
32
|
+
export declare function trimSlashes(value: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Whether a value is a plain JSON object.
|
|
35
|
+
* @param value - Candidate value
|
|
36
|
+
* @returns True when the value is a non-null, non-array object
|
|
37
|
+
*/
|
|
38
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Wrap non-blocking observations as findings. They deliberately carry no
|
|
41
|
+
* `blocker` key: naming one would stand a blocker up — and flip the whole
|
|
42
|
+
* repository to `NOT_READY` — on an observation that was never decidable.
|
|
43
|
+
* @param notes - Informational lines
|
|
44
|
+
* @returns Findings, one per note
|
|
45
|
+
*/
|
|
46
|
+
export declare function informationalFindings(notes: readonly string[]): readonly Record<string, unknown>[];
|
|
47
|
+
//# sourceMappingURL=doctor-readiness-shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-shared.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-shared.ts"],"names":[],"mappings":"AAeA;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMxB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,OAAO,CAAC,CAKlB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAEpC"}
|