@codyswann/lisa 2.280.0 → 2.282.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/all/copy-contents/gitignore +2 -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-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-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.d.ts.map +1 -1
- package/dist/cli/doctor-readiness.js +18 -12
- package/dist/cli/doctor-readiness.js.map +1 -1
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +20 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/standards-proof-cmd.d.ts +7 -0
- package/dist/cli/standards-proof-cmd.d.ts.map +1 -0
- package/dist/cli/standards-proof-cmd.js +14 -0
- package/dist/cli/standards-proof-cmd.js.map +1 -0
- package/dist/cli/ui-setup-readiness-local.d.ts +2 -1
- package/dist/cli/ui-setup-readiness-local.d.ts.map +1 -1
- package/dist/cli/ui-setup-readiness-local.js +4 -2
- package/dist/cli/ui-setup-readiness-local.js.map +1 -1
- package/dist/cli/ui-setup-readiness.d.ts.map +1 -1
- package/dist/cli/ui-setup-readiness.js +3 -2
- package/dist/cli/ui-setup-readiness.js.map +1 -1
- package/dist/cli/update-check-hook.d.ts +19 -0
- package/dist/cli/update-check-hook.d.ts.map +1 -0
- package/dist/cli/update-check-hook.js +36 -0
- package/dist/cli/update-check-hook.js.map +1 -0
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +47 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/standards/capture.d.ts +41 -0
- package/dist/standards/capture.d.ts.map +1 -0
- package/dist/standards/capture.js +196 -0
- package/dist/standards/capture.js.map +1 -0
- package/dist/standards/contract.d.ts +45 -0
- package/dist/standards/contract.d.ts.map +1 -0
- package/dist/standards/contract.js +175 -0
- package/dist/standards/contract.js.map +1 -0
- package/dist/standards/git-state.d.ts +30 -0
- package/dist/standards/git-state.d.ts.map +1 -0
- package/dist/standards/git-state.js +179 -0
- package/dist/standards/git-state.js.map +1 -0
- package/dist/standards/index.d.ts +8 -0
- package/dist/standards/index.d.ts.map +1 -0
- package/dist/standards/index.js +8 -0
- package/dist/standards/index.js.map +1 -0
- package/dist/standards/readiness.d.ts +11 -0
- package/dist/standards/readiness.d.ts.map +1 -0
- package/dist/standards/readiness.js +170 -0
- package/dist/standards/readiness.js.map +1 -0
- package/dist/standards/registry.d.ts +42 -0
- package/dist/standards/registry.d.ts.map +1 -0
- package/dist/standards/registry.js +330 -0
- package/dist/standards/registry.js.map +1 -0
- package/dist/standards/storage-reasons.d.ts +15 -0
- package/dist/standards/storage-reasons.d.ts.map +1 -0
- package/dist/standards/storage-reasons.js +48 -0
- package/dist/standards/storage-reasons.js.map +1 -0
- package/dist/standards/storage.d.ts +36 -0
- package/dist/standards/storage.d.ts.map +1 -0
- package/dist/standards/storage.js +330 -0
- package/dist/standards/storage.js.map +1 -0
- package/dist/standards/test-evidence.d.ts +19 -0
- package/dist/standards/test-evidence.d.ts.map +1 -0
- package/dist/standards/test-evidence.js +195 -0
- package/dist/standards/test-evidence.js.map +1 -0
- 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/.codex-plugin/skills/lisa-doctor/SKILL.md +21 -3
- package/plugins/lisa/scripts/doctor-report.mjs +266 -22
- package/plugins/lisa/skills/lisa-doctor/SKILL.md +21 -3
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/doctor-report.mjs +266 -22
- package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +21 -3
- 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 +266 -22
- package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +21 -3
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/scripts/doctor-report.mjs +266 -22
- package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +21 -3
- 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
- package/plugins/src/base/scripts/doctor-report.mjs +266 -22
- package/plugins/src/base/skills/lisa-doctor/SKILL.md +21 -3
- package/rails/copy-overwrite/lefthook.yml +9 -11
- package/rails/copy-overwrite/scripts/lisa-clean-git-env.sh +13 -0
- package/typescript/copy-contents/.husky/pre-push +9 -0
- package/ui/index.html +3 -2
|
@@ -215,6 +215,8 @@ expo-env.d.ts
|
|
|
215
215
|
.lisa/verification-status.json
|
|
216
216
|
# Latest Health v1 result is local runtime state, never project configuration.
|
|
217
217
|
.lisa/health/latest.json
|
|
218
|
+
# Freshness-bound standards proof is local runtime evidence, not knowledge.
|
|
219
|
+
.lisa/standards/latest.json
|
|
218
220
|
|
|
219
221
|
# Automation run outcomes — local scheduler observations read by
|
|
220
222
|
# /lisa:automation-status. Runbooks are project knowledge; run records are not.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capabilities/tools readiness producer (PRD #1739, #1896).
|
|
3
|
+
*
|
|
4
|
+
* Dimension 2 of the `readiness-rubric` asks whether every tool the work needs
|
|
5
|
+
* is provably *reachable*, not merely installed. The `tool-access-gate` names
|
|
6
|
+
* the anti-pattern explicitly: a binary on `PATH`, a dependency in a manifest,
|
|
7
|
+
* or a configured MCP server proves that something was declared, never that a
|
|
8
|
+
* request through it succeeds. Reachability is established by a live read-only
|
|
9
|
+
* probe against the real system, and nothing an offline file read can see is a
|
|
10
|
+
* substitute for one.
|
|
11
|
+
*
|
|
12
|
+
* So this dimension has no offline evidence to gather and no blocker to stand.
|
|
13
|
+
* What it has is an obligation to say so: a blank dimension reads as "nothing to
|
|
14
|
+
* report" when it actually means "never looked" (#1898), and a presence-based
|
|
15
|
+
* PASS would be worse still — it would report the anti-pattern as proof. The
|
|
16
|
+
* producer therefore renders one reasoned SKIP, every time, naming the live
|
|
17
|
+
* probe it did not run.
|
|
18
|
+
* @module cli/doctor-readiness-capabilities
|
|
19
|
+
*/
|
|
20
|
+
import type { ReadinessDimensionRecord } from "./doctor-readiness-types.js";
|
|
21
|
+
/** The capabilities/tools readiness dimension id (readiness-rubric, RRR-1). */
|
|
22
|
+
export declare const CAPABILITIES_TOOLS_DIMENSION_ID = "capabilities-tools";
|
|
23
|
+
/**
|
|
24
|
+
* Assess the capabilities/tools dimension. Always a reasoned SKIP: the question
|
|
25
|
+
* is not offline-answerable, so the honest answer is a stated non-answer. The
|
|
26
|
+
* record names no `blocker`, so it can never stand one up, and the `root`
|
|
27
|
+
* argument is accepted only to satisfy the shared producer signature — reading
|
|
28
|
+
* it would be the presence anti-pattern the rule forbids.
|
|
29
|
+
* @param _root - Project root (deliberately unread)
|
|
30
|
+
* @returns The capabilities/tools dimension record
|
|
31
|
+
*/
|
|
32
|
+
export declare function assessCapabilitiesToolsDimension(_root: string): Promise<ReadinessDimensionRecord>;
|
|
33
|
+
//# sourceMappingURL=doctor-readiness-capabilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-capabilities.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-capabilities.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAE5E,+EAA+E;AAC/E,eAAO,MAAM,+BAA+B,uBAAuB,CAAC;AAWpE;;;;;;;;GAQG;AACH,wBAAsB,gCAAgC,CACpD,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,wBAAwB,CAAC,CAMnC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The capabilities/tools readiness dimension id (readiness-rubric, RRR-1). */
|
|
2
|
+
export const CAPABILITIES_TOOLS_DIMENSION_ID = "capabilities-tools";
|
|
3
|
+
/** The stated reason this dimension is never assessed by an offline read. */
|
|
4
|
+
const CAPABILITIES_SKIP_REASON = "Tool reachability requires a live read-only probe against the real system " +
|
|
5
|
+
"and cannot be established from an offline read. The `tool-access-gate` rule " +
|
|
6
|
+
"names presence — a binary on `PATH`, a dependency in a manifest, a " +
|
|
7
|
+
"configured MCP server — as the anti-pattern: it proves a tool was declared, " +
|
|
8
|
+
"never that a request through it succeeds. This dimension was therefore " +
|
|
9
|
+
"deliberately not assessed rather than answered from presence.";
|
|
10
|
+
/**
|
|
11
|
+
* Assess the capabilities/tools dimension. Always a reasoned SKIP: the question
|
|
12
|
+
* is not offline-answerable, so the honest answer is a stated non-answer. The
|
|
13
|
+
* record names no `blocker`, so it can never stand one up, and the `root`
|
|
14
|
+
* argument is accepted only to satisfy the shared producer signature — reading
|
|
15
|
+
* it would be the presence anti-pattern the rule forbids.
|
|
16
|
+
* @param _root - Project root (deliberately unread)
|
|
17
|
+
* @returns The capabilities/tools dimension record
|
|
18
|
+
*/
|
|
19
|
+
export async function assessCapabilitiesToolsDimension(_root) {
|
|
20
|
+
return await Promise.resolve({
|
|
21
|
+
id: CAPABILITIES_TOOLS_DIMENSION_ID,
|
|
22
|
+
status: "SKIP",
|
|
23
|
+
findings: [{ reason: CAPABILITIES_SKIP_REASON, skip: true }],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=doctor-readiness-capabilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-capabilities.js","sourceRoot":"","sources":["../../src/cli/doctor-readiness-capabilities.ts"],"names":[],"mappings":"AAqBA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,+BAA+B,GAAG,oBAAoB,CAAC;AAEpE,6EAA6E;AAC7E,MAAM,wBAAwB,GAC5B,4EAA4E;IAC5E,8EAA8E;IAC9E,qEAAqE;IACrE,8EAA8E;IAC9E,yEAAyE;IACzE,+DAA+D,CAAC;AAElE;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gCAAgC,CACpD,KAAa;IAEb,OAAO,MAAM,OAAO,CAAC,OAAO,CAAC;QAC3B,EAAE,EAAE,+BAA+B;QACnC,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,wBAAwB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;KAC7D,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReadinessDimensionRecord } from "./doctor-readiness-types.js";
|
|
2
|
+
import { type ParsedWorkflow } from "./doctor-readiness-workflows.js";
|
|
3
|
+
/** The domain-ownership readiness dimension id (readiness-rubric, RRR-1). */
|
|
4
|
+
export declare const DOMAIN_OWNERSHIP_DIMENSION_ID = "domain-ownership";
|
|
5
|
+
/**
|
|
6
|
+
* Assess the domain-ownership dimension: B1, "a realistic path causes silent
|
|
7
|
+
* data loss". Offline by construction — it reads only the repository's declared
|
|
8
|
+
* workflows — and deliberately conservative: it stands B1 only when a workflow
|
|
9
|
+
* file alone proves an irreversible data-destroying command runs unattended,
|
|
10
|
+
* ungated, and with no way back, and renders a stated-reason SKIP in every other
|
|
11
|
+
* case, including the clean one.
|
|
12
|
+
* @param root - Project root to assess
|
|
13
|
+
* @param parsedWorkflows - Pre-parsed workflows (default: parse `root`)
|
|
14
|
+
* @returns The domain-ownership dimension record
|
|
15
|
+
*/
|
|
16
|
+
export declare function assessDomainOwnershipDimension(root: string, parsedWorkflows?: readonly ParsedWorkflow[]): Promise<ReadinessDimensionRecord>;
|
|
17
|
+
//# sourceMappingURL=doctor-readiness-domain.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-domain.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-domain.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,iCAAiC,CAAC;AAEzC,6EAA6E;AAC7E,eAAO,MAAM,6BAA6B,qBAAqB,CAAC;AAiKhE;;;;;;;;;;GAUG;AACH,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAC1C,OAAO,CAAC,wBAAwB,CAAC,CAyBnC"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The domain-ownership readiness producer — ship blocker B1 (PRD #1739, #1896).
|
|
3
|
+
*
|
|
4
|
+
* Dimension 3 of the `readiness-rubric` asks whether the business rules,
|
|
5
|
+
* glossary, and danger zones are owned and written down. B1 asks the sharpest
|
|
6
|
+
* consequence of getting that wrong: does a realistic path destroy or corrupt
|
|
7
|
+
* data with no error surfaced and no way back?
|
|
8
|
+
*
|
|
9
|
+
* That question is the hardest one in the rubric to answer honestly offline, and
|
|
10
|
+
* getting it wrong is expensive in exactly one direction. A standing blocker
|
|
11
|
+
* flips the whole repository to `NOT_READY` regardless of this dimension's
|
|
12
|
+
* status — `WARN` is a confidence signal to a human reader, never a softener —
|
|
13
|
+
* so a false B1 on an ordinary brownfield repository would destroy the trust
|
|
14
|
+
* this entire feature exists to build. Three disciplines follow:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Only an unambiguous, automated, ungated, unrecoverable destruction of
|
|
17
|
+
* data stands B1.** The command must irreversibly destroy *data* (not build
|
|
18
|
+
* output), inside a job an automated trigger reaches, with no `if:`, no
|
|
19
|
+
* `environment:`, no backup step beside it, and no checked-in runbook.
|
|
20
|
+
* 2. **Presence of a destructive surface is never a violation.** A `migrations/`
|
|
21
|
+
* directory, a reversible migration that drops a table, a guarded command, a
|
|
22
|
+
* manually dispatched workflow, or a command aimed at an ephemeral CI target
|
|
23
|
+
* are all reported as observations at most.
|
|
24
|
+
* 3. **The clean case is a stated SKIP, never PASS.** Whether the danger zones
|
|
25
|
+
* are genuinely owned and written down cannot be established by reading
|
|
26
|
+
* files; claiming PASS would be the same overstatement B6 exists to catch.
|
|
27
|
+
* @module cli/doctor-readiness-domain
|
|
28
|
+
*/
|
|
29
|
+
import { describeCommands, hasCheckedInRunbook, looksEphemeral, scanCommands, } from "./doctor-readiness-operations.js";
|
|
30
|
+
import { informationalFindings } from "./doctor-readiness-shared.js";
|
|
31
|
+
import { parseRepositoryWorkflows, } from "./doctor-readiness-workflows.js";
|
|
32
|
+
/** The domain-ownership readiness dimension id (readiness-rubric, RRR-1). */
|
|
33
|
+
export const DOMAIN_OWNERSHIP_DIMENSION_ID = "domain-ownership";
|
|
34
|
+
/** The ship blocker for a realistic path that causes silent data loss. */
|
|
35
|
+
const DOMAIN_BLOCKER_ID = "B1";
|
|
36
|
+
/** Most destructive commands quoted in one finding before summarizing. */
|
|
37
|
+
const MAX_EVIDENCE_COMMANDS = 10;
|
|
38
|
+
/**
|
|
39
|
+
* Commands that irreversibly destroy stored data. Every entry is a whole
|
|
40
|
+
* operation, not a keyword: `rm -rf` is deliberately absent because build
|
|
41
|
+
* outputs and caches dominate its real-world use, and a blocker built on it
|
|
42
|
+
* would fire on nearly every repository that cleans a `dist/` directory.
|
|
43
|
+
*/
|
|
44
|
+
const IRREVERSIBLE_DATA_OPS = [
|
|
45
|
+
/\bdrop\s+database\b/,
|
|
46
|
+
/\bdrop\s+schema\b/,
|
|
47
|
+
/\btruncate\s+table\b/,
|
|
48
|
+
/\baws\s+s3\s+rm\b[^\n]*--recursive\b/,
|
|
49
|
+
/\baws\s+s3\s+rb\b[^\n]*--force\b/,
|
|
50
|
+
/\baws\s+dynamodb\s+delete-table\b/,
|
|
51
|
+
/\baws\s+rds\s+delete-db-(instance|cluster)\b[^\n]*--skip-final-snapshot\b/,
|
|
52
|
+
];
|
|
53
|
+
/** Commands that create the way back: a copy taken before the destruction. */
|
|
54
|
+
const RECOVERY_COMMANDS = [
|
|
55
|
+
/\bpg_dump\b/,
|
|
56
|
+
/\bmysqldump\b/,
|
|
57
|
+
/\bbackup\b/,
|
|
58
|
+
/\bsnapshot\b/,
|
|
59
|
+
/\baws\s+s3\s+sync\b/,
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Whether a command irreversibly destroys data that is not obviously throwaway.
|
|
63
|
+
*
|
|
64
|
+
* The ephemeral screen reads the job's `env:` block alongside the command,
|
|
65
|
+
* because the evidence is routinely outside the command text: `psql
|
|
66
|
+
* "$DATABASE_URL" -c "DROP SCHEMA public CASCADE"` says nothing on its own,
|
|
67
|
+
* while the `DATABASE_URL` it expands to may point at `127.0.0.1` and settle the
|
|
68
|
+
* question outright.
|
|
69
|
+
* @param command - The lower-cased `run` command
|
|
70
|
+
* @param job - The job the command runs in
|
|
71
|
+
* @returns True when the command is an unambiguous data-destroying operation
|
|
72
|
+
*/
|
|
73
|
+
function destroysData(command, job) {
|
|
74
|
+
return (IRREVERSIBLE_DATA_OPS.some(pattern => pattern.test(command)) &&
|
|
75
|
+
!looksEphemeral(`${command}\n${job.env}`));
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* State why a job that boots its own `services:` containers cannot be settled.
|
|
79
|
+
* A job that starts its own database is operating on state it created moments
|
|
80
|
+
* earlier, so destroying it is not data loss — but proving *which* store the
|
|
81
|
+
* command targets is beyond an offline read, so this is deferred, not cleared.
|
|
82
|
+
* @param job - The job to consider
|
|
83
|
+
* @returns A stated reason, or null when the job starts no services
|
|
84
|
+
*/
|
|
85
|
+
function servicesDeferral(job) {
|
|
86
|
+
if (job.services.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return (`Job \`${job.id}\` runs a destructive data operation while starting its own ` +
|
|
90
|
+
`\`services:\` container(s) (${job.services.join(", ")}), which are created ` +
|
|
91
|
+
"and thrown away inside the run, so whether the command targets durable " +
|
|
92
|
+
"data is not established either way");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Whether a job takes its own copy before destroying anything — the way back,
|
|
96
|
+
* sitting right beside the danger.
|
|
97
|
+
* @param job - The parsed job
|
|
98
|
+
* @returns True when any step in the job looks like a backup
|
|
99
|
+
*/
|
|
100
|
+
function jobTakesBackup(job) {
|
|
101
|
+
return job.steps.some(step => RECOVERY_COMMANDS.some(pattern => pattern.test(step.run.toLowerCase())));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the rubric-shaped B1 finding from the destructive commands it cites.
|
|
105
|
+
* @param scan - The destructive-operation scan
|
|
106
|
+
* @returns The B1 finding
|
|
107
|
+
*/
|
|
108
|
+
function domainFinding(scan) {
|
|
109
|
+
return {
|
|
110
|
+
blocker: DOMAIN_BLOCKER_ID,
|
|
111
|
+
invariant_violated: "no realistic path destroys or corrupts data without surfacing an error " +
|
|
112
|
+
"and leaving a way back",
|
|
113
|
+
evidence: describeCommands(scan.ungated, MAX_EVIDENCE_COMMANDS),
|
|
114
|
+
why_proof_missed: "the destructive command is exercised only against real data, so no test " +
|
|
115
|
+
"run and no review step ever observes what it removes — the pipeline " +
|
|
116
|
+
"reports green whether the data survived or not",
|
|
117
|
+
root_correction: "put the operation behind an explicit gate (a job `environment:` or a " +
|
|
118
|
+
"confirmation input) and give it a way back — a snapshot taken in the " +
|
|
119
|
+
"same job, or a checked-in runbook naming the restore procedure",
|
|
120
|
+
machinery_to_remove: [
|
|
121
|
+
"any unattended destructive step that exists only because no retention " +
|
|
122
|
+
"or lifecycle policy was configured on the store itself",
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* State why the dimension was not settled, naming what WAS looked at so an
|
|
128
|
+
* operator can tell "nothing to report" from "never looked".
|
|
129
|
+
* @param inspected - How many workflow files were read
|
|
130
|
+
* @returns The SKIP reason
|
|
131
|
+
*/
|
|
132
|
+
function skipReason(inspected) {
|
|
133
|
+
return (`Read ${inspected} workflow file(s) for irreversible data-destroying ` +
|
|
134
|
+
"operations and found none running unattended and ungated. Whether this " +
|
|
135
|
+
"repository's business rules, glossary, and danger zones are genuinely " +
|
|
136
|
+
"owned and written down cannot be established by reading files offline — " +
|
|
137
|
+
"that needs the agent-ready domain phase — so domain ownership is not " +
|
|
138
|
+
"established either way.");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Build the SKIP record, carrying every guarded destructive operation as an
|
|
142
|
+
* observation. Those observations name no `blocker`: they are the operator's
|
|
143
|
+
* map of where the danger lives, not a verdict about it.
|
|
144
|
+
* @param guarded - The destructive commands something already guards
|
|
145
|
+
* @param scan - The destructive-operation scan
|
|
146
|
+
* @param inspected - How many workflow files were read
|
|
147
|
+
* @returns The SKIP dimension record
|
|
148
|
+
*/
|
|
149
|
+
function skipRecord(guarded, scan, inspected) {
|
|
150
|
+
return {
|
|
151
|
+
id: DOMAIN_OWNERSHIP_DIMENSION_ID,
|
|
152
|
+
status: "SKIP",
|
|
153
|
+
findings: [
|
|
154
|
+
{ reason: skipReason(inspected), skip: true },
|
|
155
|
+
...informationalFindings([
|
|
156
|
+
...guarded.map(entry => `\`${entry.workflow}\` job \`${entry.jobId}\` runs a destructive ` +
|
|
157
|
+
`operation (\`${entry.command}\`) behind a gate, beside its own ` +
|
|
158
|
+
"backup, or in a repository that checks in a recovery runbook, so " +
|
|
159
|
+
"it was not treated as a silent-data-loss path"),
|
|
160
|
+
...scan.unresolved,
|
|
161
|
+
]),
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Assess the domain-ownership dimension: B1, "a realistic path causes silent
|
|
167
|
+
* data loss". Offline by construction — it reads only the repository's declared
|
|
168
|
+
* workflows — and deliberately conservative: it stands B1 only when a workflow
|
|
169
|
+
* file alone proves an irreversible data-destroying command runs unattended,
|
|
170
|
+
* ungated, and with no way back, and renders a stated-reason SKIP in every other
|
|
171
|
+
* case, including the clean one.
|
|
172
|
+
* @param root - Project root to assess
|
|
173
|
+
* @param parsedWorkflows - Pre-parsed workflows (default: parse `root`)
|
|
174
|
+
* @returns The domain-ownership dimension record
|
|
175
|
+
*/
|
|
176
|
+
export async function assessDomainOwnershipDimension(root, parsedWorkflows) {
|
|
177
|
+
const workflows = parsedWorkflows ?? (await parseRepositoryWorkflows(root));
|
|
178
|
+
const scan = scanCommands(workflows, destroysData, {
|
|
179
|
+
exemptJob: jobTakesBackup,
|
|
180
|
+
unresolvedJob: servicesDeferral,
|
|
181
|
+
});
|
|
182
|
+
// A checked-in runbook is a way back, so it clears the "no recovery" half of
|
|
183
|
+
// B1 for the whole repository — the blocker asks for BOTH halves at once.
|
|
184
|
+
const recoverable = await hasCheckedInRunbook(root);
|
|
185
|
+
if (scan.ungated.length === 0 || recoverable) {
|
|
186
|
+
return skipRecord(recoverable ? [...scan.gated, ...scan.ungated] : scan.gated, scan, workflows.length);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
id: DOMAIN_OWNERSHIP_DIMENSION_ID,
|
|
190
|
+
// `WARN` reports how much confidence an offline read can carry; it does NOT
|
|
191
|
+
// soften the verdict. The blocker engine never reads this status, so the
|
|
192
|
+
// finding below flips the repository to `NOT_READY` exactly as `FAIL` would.
|
|
193
|
+
status: "WARN",
|
|
194
|
+
findings: [domainFinding(scan), ...informationalFindings(scan.unresolved)],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=doctor-readiness-domain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-domain.js","sourceRoot":"","sources":["../../src/cli/doctor-readiness-domain.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReadinessDimensionRecord } from "./doctor-readiness-types.js";
|
|
2
|
+
import { type ParsedWorkflow } from "./doctor-readiness-workflows.js";
|
|
3
|
+
/** The feedback/guardrails readiness dimension id (readiness-rubric, RRR-1). */
|
|
4
|
+
export declare const FEEDBACK_GUARDRAILS_DIMENSION_ID = "feedback-guardrails";
|
|
5
|
+
/**
|
|
6
|
+
* Assess the feedback/guardrails dimension: B4, "a consequential operation has
|
|
7
|
+
* no gate and no recovery". Offline by construction, and honest about it — the
|
|
8
|
+
* environment-protection-rule half is reasoned-SKIPped in every record, so this
|
|
9
|
+
* dimension never reports `PASS`. B4 stands only on the file-provable subset.
|
|
10
|
+
* @param root - Project root to assess
|
|
11
|
+
* @param parsedWorkflows - Pre-parsed workflows (default: parse `root`)
|
|
12
|
+
* @returns The feedback/guardrails dimension record
|
|
13
|
+
*/
|
|
14
|
+
export declare function assessFeedbackGuardrailsDimension(root: string, parsedWorkflows?: readonly ParsedWorkflow[]): Promise<ReadinessDimensionRecord>;
|
|
15
|
+
//# sourceMappingURL=doctor-readiness-guardrails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-readiness-guardrails.d.ts","sourceRoot":"","sources":["../../src/cli/doctor-readiness-guardrails.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAC5E,OAAO,EACL,KAAK,cAAc,EAGpB,MAAM,iCAAiC,CAAC;AAEzC,gFAAgF;AAChF,eAAO,MAAM,gCAAgC,wBAAwB,CAAC;AA0LtE;;;;;;;;GAQG;AACH,wBAAsB,iCAAiC,CACrD,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,GAC1C,OAAO,CAAC,wBAAwB,CAAC,CA6BnC"}
|
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EAEd,YAAY,GACb,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAErE,OAAO,EAGL,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AAEzC,gFAAgF;AAChF,MAAM,CAAC,MAAM,gCAAgC,GAAG,qBAAqB,CAAC;AAEtE,mFAAmF;AACnF,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,8EAA8E;AAC9E,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAEjC;;;;GAIG;AACH,MAAM,4BAA4B,GAChC,uEAAuE;IACvE,wEAAwE;IACxE,uEAAuE;IACvE,8EAA8E;IAC9E,qEAAqE;IACrE,2DAA2D,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,iBAAiB,GAAsB;IAC3C,qDAAqD;IACrD,kCAAkC;IAClC,mBAAmB;IACnB,sBAAsB;IACtB,8BAA8B;IAC9B,yBAAyB;CAC1B,CAAC;AAEF;;;;GAIG;AACH,MAAM,kBAAkB,GAAsB;IAC5C,qBAAqB;IACrB,sBAAsB;IACtB,mBAAmB;IACnB,uBAAuB;CACxB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,kBAAkB,GAAsB;IAC5C,8EAA8E;IAC9E,0BAA0B;IAC1B,2BAA2B;CAC5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,SAAS,eAAe,CAAC,OAAe,EAAE,GAAsB;IAC9D,IAAI,cAAc,CAAC,GAAG,OAAO,KAAK,GAAG,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;QAC7C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,CACL,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxD,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxD,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAC7D,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,0BAA0B,GAAsB;IACpD,cAAc;IACd,qBAAqB;IACrB,QAAQ;CACT,CAAC;AAEF;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,QAAwB;IACjD,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC/C,0BAA0B,CAAC,QAAQ,CAAC,KAAK,CAAC,CAC3C,CAAC;IACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CACL,KAAK,QAAQ,CAAC,IAAI,8CAA8C,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;QACtF,6EAA6E;QAC7E,sEAAsE;QACtE,yDAAyD,CAC1D,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,iBAAiB,CAAC,IAAmB;IAC5C,OAAO;QACL,OAAO,EAAE,qBAAqB;QAC9B,kBAAkB,EAChB,uEAAuE;YACvE,mBAAmB;QACrB,QAAQ,EAAE,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,qBAAqB,CAAC;QAC/D,gBAAgB,EACd,uEAAuE;YACvE,wEAAwE;YACxE,yEAAyE;YACzE,mDAAmD;QACrD,eAAe,EACb,yEAAyE;YACzE,mEAAmE;YACnE,sEAAsE;QACxE,mBAAmB,EAAE;YACnB,wEAAwE;gBACtE,mDAAmD;SACtD;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,UAAU,CACjB,IAAmB,EACnB,SAAiB,EACjB,OAAgB;IAEhB,OAAO;QACL,EAAE,EAAE,gCAAgC;QACpC,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE;YACR;gBACE,MAAM,EACJ,QAAQ,SAAS,kDAAkD;oBACnE,6DAA6D;oBAC7D,4DAA4D;oBAC5D,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,MAAM,4BAA4B,EAAE;gBACvE,IAAI,EAAE,IAAI;aACX;YACD,GAAG,qBAAqB,CAAC;gBACvB,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CACf,KAAK,CAAC,EAAE,CACN,KAAK,KAAK,CAAC,QAAQ,YAAY,KAAK,CAAC,KAAK,0BAA0B;oBACpE,gBAAgB,KAAK,CAAC,OAAO,wCAAwC;oBACrE,mEAAmE;oBACnE,8DAA8D;oBAC9D,4DAA4D;oBAC5D,kCAAkC,CACrC;gBACD,GAAG,IAAI,CAAC,UAAU;aACnB,CAAC;SACH;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,IAAY,EACZ,eAA2C;IAE3C,MAAM,SAAS,GACb,eAAe,IAAI,CAAC,MAAM,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,YAAY,CAAC,SAAS,EAAE,eAAe,EAAE;QACpD,kBAAkB,EAAE,iBAAiB;KACtC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACzC,OAAO,UAAU,CACf,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EACrE,SAAS,CAAC,MAAM,EAChB,OAAO,CACR,CAAC;IACJ,CAAC;IACD,OAAO;QACL,EAAE,EAAE,gCAAgC;QACpC,4EAA4E;QAC5E,kEAAkE;QAClE,MAAM,EAAE,MAAM;QACd,QAAQ,EAAE;YACR,iBAAiB,CAAC,IAAI,CAAC;YACvB,mEAAmE;YACnE,oEAAoE;YACpE,GAAG,qBAAqB,CAAC;gBACvB,4BAA4B;gBAC5B,GAAG,IAAI,CAAC,UAAU;aACnB,CAAC;SACH;KACF,CAAC;AACJ,CAAC"}
|