iosm-cli 0.1.3 → 0.2.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/CHANGELOG.md +27 -0
- package/README.md +33 -2
- package/dist/core/blast.d.ts +62 -0
- package/dist/core/blast.d.ts.map +1 -0
- package/dist/core/blast.js +448 -0
- package/dist/core/blast.js.map +1 -0
- package/dist/core/contract.d.ts +54 -0
- package/dist/core/contract.d.ts.map +1 -0
- package/dist/core/contract.js +300 -0
- package/dist/core/contract.js.map +1 -0
- package/dist/core/semantic/config.d.ts.map +1 -1
- package/dist/core/semantic/config.js +5 -0
- package/dist/core/semantic/config.js.map +1 -1
- package/dist/core/semantic/index.d.ts +1 -1
- package/dist/core/semantic/index.d.ts.map +1 -1
- package/dist/core/semantic/index.js +1 -1
- package/dist/core/semantic/index.js.map +1 -1
- package/dist/core/semantic/runtime.d.ts.map +1 -1
- package/dist/core/semantic/runtime.js +12 -1
- package/dist/core/semantic/runtime.js.map +1 -1
- package/dist/core/semantic/types.d.ts +6 -0
- package/dist/core/semantic/types.d.ts.map +1 -1
- package/dist/core/semantic/types.js +6 -0
- package/dist/core/semantic/types.js.map +1 -1
- package/dist/core/shadow-guard.d.ts +30 -0
- package/dist/core/shadow-guard.d.ts.map +1 -0
- package/dist/core/shadow-guard.js +81 -0
- package/dist/core/shadow-guard.js.map +1 -0
- package/dist/core/singular.d.ts +73 -0
- package/dist/core/singular.d.ts.map +1 -0
- package/dist/core/singular.js +413 -0
- package/dist/core/singular.js.map +1 -0
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +9 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +3 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/semantic-search.d.ts.map +1 -1
- package/dist/core/tools/semantic-search.js +1 -0
- package/dist/core/tools/semantic-search.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +8 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +8 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +70 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +43 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1304 -24
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/cli-reference.md +19 -1
- package/docs/configuration.md +5 -0
- package/docs/interactive-mode.md +131 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
9
9
|
|
|
10
10
|
_No unreleased changes._
|
|
11
11
|
|
|
12
|
+
## [0.2.0] - 2026-03-11
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Interactive engineering contract manager (`/contract`)** — field-by-field contract editing with immediate save-on-enter and automatic JSON generation for project scope
|
|
17
|
+
- **Layered contract model** — explicit `project`, `session`, and `effective` contract layers with copy/delete flows and merged runtime enforcement
|
|
18
|
+
- **Singular feasibility mode (`/singular`)** — command-first feasibility analysis that combines repository baseline scan with a standard agent pass and returns exactly three implementation options
|
|
19
|
+
- **Option-driven execution handoff** — `/singular` now produces concrete file targets, step plans, trade-offs, and decision guidance before implementation starts
|
|
20
|
+
- **Regression coverage for large paste UX** — multiline unbracketed paste now covered by dedicated tests to ensure one submission flow and compact marker rendering
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Feasibility workflow naming** — `/blast` replaced by `/singular` for feature feasibility decisions
|
|
25
|
+
- **Profile cleanup** — `/shadow` workflow removed to avoid duplication with plan-oriented analysis
|
|
26
|
+
- **Contract interaction model** — removed extra save step in field editor; entering value immediately persists to selected scope
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **TUI width safety** — startup resources block now truncates long lines to terminal width, preventing render crashes on narrow terminals
|
|
31
|
+
- **Paste queue behavior** — large pasted multiline input is treated as a single paste event instead of fragmented queued submissions
|
|
32
|
+
|
|
33
|
+
### Documentation
|
|
34
|
+
|
|
35
|
+
- Expanded README with dedicated decision workflow section (`/contract` vs `/singular`), command migration notes, and clearer contract layer distinctions
|
|
36
|
+
- Extended interactive mode docs with explicit `effective/session/project` explanations and migration guidance from removed commands
|
|
37
|
+
- Updated CLI reference with interactive feasibility/contract command behavior and migration notes
|
|
38
|
+
|
|
12
39
|
## [0.1.3] - 2026-03-10
|
|
13
40
|
|
|
14
41
|
### Added
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<h1 align="center">IOSM CLI v0.
|
|
1
|
+
<h1 align="center">IOSM CLI v0.2.0</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<strong>AI Engineering Runtime for Professional Developers</strong>
|
|
@@ -141,7 +141,7 @@ Core commands to unlock full runtime value:
|
|
|
141
141
|
|
|
142
142
|
```console
|
|
143
143
|
$ iosm
|
|
144
|
-
IOSM CLI v0.
|
|
144
|
+
IOSM CLI v0.2.0 [full]
|
|
145
145
|
|
|
146
146
|
you> Refactor authentication module with parallel agents, then finalize in IOSM mode
|
|
147
147
|
iosm> /orchestrate --parallel --agents 4 \
|
|
@@ -260,11 +260,42 @@ Track and resume delegated execution with `/subagent-runs`, `/subagent-resume`,
|
|
|
260
260
|
| Track delegated runs | `/subagent-runs`, `/subagent-resume`, `/team-runs`, `/team-status` | Monitor and resume orchestration pipelines |
|
|
261
261
|
| Manage MCP servers | `/mcp` | Inspect/add/enable external tool servers interactively |
|
|
262
262
|
| Manage semantic search | `/semantic` | Configure provider with auto model discovery (OpenRouter/Ollama), index codebase, query by intent/meaning |
|
|
263
|
+
| Define engineering contract | `/contract` | Field-by-field interactive contract editor with auto-save and automatic JSON generation |
|
|
264
|
+
| Analyze feasibility variants | `/singular <feature request>` | Runs baseline + standard agent pass, then returns 3 implementation options and recommendation |
|
|
263
265
|
| Manage memory | `/memory` | Add/edit/remove persistent project facts and constraints |
|
|
264
266
|
| Save/restore state | `/checkpoint` / `/rollback` | Safe experimentation with fast rollback |
|
|
265
267
|
| Diagnose runtime | `/doctor` | Verify model/auth/MCP/resources when behavior is inconsistent |
|
|
266
268
|
| Manage settings | `/settings` | Tune runtime defaults and operational preferences |
|
|
267
269
|
|
|
270
|
+
## Decision Workflow: `/contract` + `/singular`
|
|
271
|
+
|
|
272
|
+
### `/contract` (interactive contract manager)
|
|
273
|
+
|
|
274
|
+
- No manual JSON editing in terminal.
|
|
275
|
+
- You edit fields directly (`goal`, `scope_include`, `scope_exclude`, `constraints`, `quality_gates`, `definition_of_done`, `risks`, and additional planning fields).
|
|
276
|
+
- Press `Enter` on a field value and it is saved immediately.
|
|
277
|
+
- Contract JSON is built automatically.
|
|
278
|
+
|
|
279
|
+
Key manager actions:
|
|
280
|
+
- `Open effective contract` = read merged runtime contract (`project + session`).
|
|
281
|
+
- `Edit session contract` = temporary overlay for current session only.
|
|
282
|
+
- `Edit project contract` = persistent baseline in `.iosm/contract.json`.
|
|
283
|
+
|
|
284
|
+
### `/singular <request>` (feature feasibility analyzer)
|
|
285
|
+
|
|
286
|
+
- Command-first flow: write request, run analysis, receive decision options.
|
|
287
|
+
- Uses standard agent-style repository run (not static form output), then merges with baseline repository scan.
|
|
288
|
+
- Produces exactly 3 options:
|
|
289
|
+
- `Option 1`: practical implementation path (usually recommended).
|
|
290
|
+
- `Option 2`: alternative strategy with different trade-offs.
|
|
291
|
+
- `Option 3`: defer/do-not-implement-now path.
|
|
292
|
+
- Each option includes affected files, step-by-step plan, risks, and when-to-choose guidance.
|
|
293
|
+
- User selects `1/2/3` (or exits) before coding starts.
|
|
294
|
+
|
|
295
|
+
Legacy note:
|
|
296
|
+
- `/blast` and `/shadow` are removed from active workflow.
|
|
297
|
+
- Use `/singular` for feasibility decisions and `/contract` for engineering constraints.
|
|
298
|
+
|
|
268
299
|
## IOSM In One Line
|
|
269
300
|
|
|
270
301
|
**IOSM** gives you a repeatable loop for improving codebases with explicit quality gates, metrics, and artifact history instead of one-off AI edits.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EngineeringContract } from "./contract.js";
|
|
2
|
+
export type BlastProfile = "quick" | "full";
|
|
3
|
+
export type BlastSeverity = "low" | "medium" | "high";
|
|
4
|
+
export interface BlastFinding {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
severity: BlastSeverity;
|
|
8
|
+
category: string;
|
|
9
|
+
detail: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
line?: number;
|
|
12
|
+
recommendation?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface BlastRunOptions {
|
|
15
|
+
profile: BlastProfile;
|
|
16
|
+
autosave?: boolean;
|
|
17
|
+
contract?: EngineeringContract;
|
|
18
|
+
}
|
|
19
|
+
export interface BlastRunResult {
|
|
20
|
+
runId: string;
|
|
21
|
+
profile: BlastProfile;
|
|
22
|
+
startedAt: string;
|
|
23
|
+
completedAt: string;
|
|
24
|
+
durationMs: number;
|
|
25
|
+
scannedFiles: number;
|
|
26
|
+
scannedLines: number;
|
|
27
|
+
findings: BlastFinding[];
|
|
28
|
+
summary: string;
|
|
29
|
+
nextSteps: string[];
|
|
30
|
+
reportMarkdown: string;
|
|
31
|
+
contract: EngineeringContract;
|
|
32
|
+
autosaved: boolean;
|
|
33
|
+
reportPath?: string;
|
|
34
|
+
findingsPath?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BlastLastRun {
|
|
37
|
+
runId: string;
|
|
38
|
+
reportPath: string;
|
|
39
|
+
findingsPath: string;
|
|
40
|
+
metaPath?: string;
|
|
41
|
+
summary?: string;
|
|
42
|
+
profile?: BlastProfile;
|
|
43
|
+
completedAt?: string;
|
|
44
|
+
findings?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface BlastServiceOptions {
|
|
47
|
+
cwd: string;
|
|
48
|
+
}
|
|
49
|
+
export declare class BlastService {
|
|
50
|
+
private readonly cwd;
|
|
51
|
+
constructor(options: BlastServiceOptions);
|
|
52
|
+
getAuditsRoot(): string;
|
|
53
|
+
getLastRun(): BlastLastRun | undefined;
|
|
54
|
+
run(options: BlastRunOptions): Promise<BlastRunResult>;
|
|
55
|
+
private saveRunArtifacts;
|
|
56
|
+
private scanRepository;
|
|
57
|
+
private walkFiles;
|
|
58
|
+
private buildSummary;
|
|
59
|
+
private buildNextSteps;
|
|
60
|
+
private buildReportMarkdown;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=blast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blast.d.ts","sourceRoot":"","sources":["../../src/core/blast.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;AAC5C,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtD,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAkFD,MAAM,WAAW,mBAAmB;IACnC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,OAAO,EAAE,mBAAmB;IAIxC,aAAa,IAAI,MAAM;IAIvB,UAAU,IAAI,YAAY,GAAG,SAAS;IA+ChC,GAAG,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IAqD5D,OAAO,CAAC,gBAAgB;IAiCxB,OAAO,CAAC,cAAc;IAgKtB,OAAO,CAAC,SAAS;IAiCjB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,mBAAmB;CAoE3B"}
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
const SCAN_TEXT_EXTENSIONS = new Set([
|
|
4
|
+
".ts",
|
|
5
|
+
".tsx",
|
|
6
|
+
".js",
|
|
7
|
+
".jsx",
|
|
8
|
+
".mjs",
|
|
9
|
+
".cjs",
|
|
10
|
+
".py",
|
|
11
|
+
".go",
|
|
12
|
+
".rs",
|
|
13
|
+
".java",
|
|
14
|
+
".json",
|
|
15
|
+
".yaml",
|
|
16
|
+
".yml",
|
|
17
|
+
".toml",
|
|
18
|
+
".md",
|
|
19
|
+
".sh",
|
|
20
|
+
".env",
|
|
21
|
+
".sql",
|
|
22
|
+
".html",
|
|
23
|
+
".css",
|
|
24
|
+
]);
|
|
25
|
+
const EXCLUDED_DIR_NAMES = new Set([".git", "node_modules", "dist", "build", ".iosm", ".next", "coverage"]);
|
|
26
|
+
function toPosixPath(value) {
|
|
27
|
+
return value.split(sep).join("/");
|
|
28
|
+
}
|
|
29
|
+
function getExtension(filePath) {
|
|
30
|
+
const normalized = filePath.toLowerCase();
|
|
31
|
+
const index = normalized.lastIndexOf(".");
|
|
32
|
+
return index >= 0 ? normalized.slice(index) : "";
|
|
33
|
+
}
|
|
34
|
+
function containsAny(text, terms) {
|
|
35
|
+
const normalized = text.toLowerCase();
|
|
36
|
+
return terms.some((term) => normalized.includes(term.toLowerCase()));
|
|
37
|
+
}
|
|
38
|
+
function countSeverity(findings, severity) {
|
|
39
|
+
return findings.filter((item) => item.severity === severity).length;
|
|
40
|
+
}
|
|
41
|
+
function clampFindings(findings, max) {
|
|
42
|
+
if (findings.length <= max)
|
|
43
|
+
return findings;
|
|
44
|
+
return findings.slice(0, max);
|
|
45
|
+
}
|
|
46
|
+
function serializeFinding(finding) {
|
|
47
|
+
const location = finding.path ? ` (${finding.path}${finding.line ? `:${finding.line}` : ""})` : "";
|
|
48
|
+
return `- [${finding.severity.toUpperCase()}] ${finding.title}${location}\n ${finding.detail}${finding.recommendation ? `\n fix: ${finding.recommendation}` : ""}`;
|
|
49
|
+
}
|
|
50
|
+
function nowIso() {
|
|
51
|
+
return new Date().toISOString();
|
|
52
|
+
}
|
|
53
|
+
function buildRunId(date = new Date()) {
|
|
54
|
+
const year = date.getUTCFullYear();
|
|
55
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
56
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
57
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
58
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
59
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
60
|
+
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
|
|
61
|
+
}
|
|
62
|
+
export class BlastService {
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.cwd = options.cwd;
|
|
65
|
+
}
|
|
66
|
+
getAuditsRoot() {
|
|
67
|
+
return join(this.cwd, ".iosm", "audits");
|
|
68
|
+
}
|
|
69
|
+
getLastRun() {
|
|
70
|
+
const root = this.getAuditsRoot();
|
|
71
|
+
if (!existsSync(root))
|
|
72
|
+
return undefined;
|
|
73
|
+
const candidates = readdirSync(root, { withFileTypes: true })
|
|
74
|
+
.filter((entry) => entry.isDirectory())
|
|
75
|
+
.map((entry) => entry.name)
|
|
76
|
+
.sort((a, b) => a.localeCompare(b));
|
|
77
|
+
const runId = candidates[candidates.length - 1];
|
|
78
|
+
if (!runId)
|
|
79
|
+
return undefined;
|
|
80
|
+
const runDir = join(root, runId);
|
|
81
|
+
const reportPath = join(runDir, "report.md");
|
|
82
|
+
const findingsPath = join(runDir, "findings.json");
|
|
83
|
+
if (!existsSync(reportPath) || !existsSync(findingsPath)) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const metaPath = join(runDir, "meta.json");
|
|
87
|
+
let summary;
|
|
88
|
+
let profile;
|
|
89
|
+
let completedAt;
|
|
90
|
+
let findingsCount;
|
|
91
|
+
if (existsSync(metaPath)) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(readFileSync(metaPath, "utf8"));
|
|
94
|
+
summary = typeof parsed.summary === "string" ? parsed.summary : undefined;
|
|
95
|
+
profile =
|
|
96
|
+
parsed.profile === "quick" || parsed.profile === "full" ? parsed.profile : undefined;
|
|
97
|
+
completedAt = typeof parsed.completedAt === "string" ? parsed.completedAt : undefined;
|
|
98
|
+
findingsCount = typeof parsed.findings === "number" ? parsed.findings : undefined;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Keep metadata optional.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
runId,
|
|
106
|
+
reportPath,
|
|
107
|
+
findingsPath,
|
|
108
|
+
metaPath: existsSync(metaPath) ? metaPath : undefined,
|
|
109
|
+
summary,
|
|
110
|
+
profile,
|
|
111
|
+
completedAt,
|
|
112
|
+
findings: findingsCount,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async run(options) {
|
|
116
|
+
const startedAt = nowIso();
|
|
117
|
+
const started = Date.now();
|
|
118
|
+
const profile = options.profile;
|
|
119
|
+
const runId = buildRunId(new Date());
|
|
120
|
+
const autosave = options.autosave !== false;
|
|
121
|
+
const contract = options.contract ? { ...options.contract } : {};
|
|
122
|
+
const { files, findings, counters } = this.scanRepository(profile, contract);
|
|
123
|
+
const findingsLimited = clampFindings(findings, profile === "full" ? 150 : 80);
|
|
124
|
+
const summary = this.buildSummary(counters, findingsLimited);
|
|
125
|
+
const nextSteps = this.buildNextSteps(findingsLimited, contract);
|
|
126
|
+
const completedAt = nowIso();
|
|
127
|
+
const durationMs = Date.now() - started;
|
|
128
|
+
const reportMarkdown = this.buildReportMarkdown({
|
|
129
|
+
runId,
|
|
130
|
+
profile,
|
|
131
|
+
startedAt,
|
|
132
|
+
completedAt,
|
|
133
|
+
durationMs,
|
|
134
|
+
counters,
|
|
135
|
+
findings: findingsLimited,
|
|
136
|
+
summary,
|
|
137
|
+
nextSteps,
|
|
138
|
+
contract,
|
|
139
|
+
});
|
|
140
|
+
const result = {
|
|
141
|
+
runId,
|
|
142
|
+
profile,
|
|
143
|
+
startedAt,
|
|
144
|
+
completedAt,
|
|
145
|
+
durationMs,
|
|
146
|
+
scannedFiles: counters.files,
|
|
147
|
+
scannedLines: counters.lines,
|
|
148
|
+
findings: findingsLimited,
|
|
149
|
+
summary,
|
|
150
|
+
nextSteps,
|
|
151
|
+
reportMarkdown,
|
|
152
|
+
contract,
|
|
153
|
+
autosaved: false,
|
|
154
|
+
};
|
|
155
|
+
if (autosave) {
|
|
156
|
+
const saved = this.saveRunArtifacts(result);
|
|
157
|
+
result.autosaved = true;
|
|
158
|
+
result.reportPath = saved.reportPath;
|
|
159
|
+
result.findingsPath = saved.findingsPath;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
saveRunArtifacts(result) {
|
|
164
|
+
const runDir = join(this.getAuditsRoot(), result.runId);
|
|
165
|
+
mkdirSync(runDir, { recursive: true });
|
|
166
|
+
const reportPath = join(runDir, "report.md");
|
|
167
|
+
const findingsPath = join(runDir, "findings.json");
|
|
168
|
+
const metaPath = join(runDir, "meta.json");
|
|
169
|
+
writeFileSync(reportPath, `${result.reportMarkdown}\n`, "utf8");
|
|
170
|
+
writeFileSync(findingsPath, `${JSON.stringify(result.findings, null, 2)}\n`, "utf8");
|
|
171
|
+
writeFileSync(metaPath, `${JSON.stringify({
|
|
172
|
+
runId: result.runId,
|
|
173
|
+
profile: result.profile,
|
|
174
|
+
summary: result.summary,
|
|
175
|
+
startedAt: result.startedAt,
|
|
176
|
+
completedAt: result.completedAt,
|
|
177
|
+
durationMs: result.durationMs,
|
|
178
|
+
files: result.scannedFiles,
|
|
179
|
+
lines: result.scannedLines,
|
|
180
|
+
findings: result.findings.length,
|
|
181
|
+
}, null, 2)}\n`, "utf8");
|
|
182
|
+
return { reportPath, findingsPath };
|
|
183
|
+
}
|
|
184
|
+
scanRepository(profile, contract) {
|
|
185
|
+
const counters = {
|
|
186
|
+
files: 0,
|
|
187
|
+
lines: 0,
|
|
188
|
+
todoCount: 0,
|
|
189
|
+
debugCount: 0,
|
|
190
|
+
unsafeEvalCount: 0,
|
|
191
|
+
anyTypeCount: 0,
|
|
192
|
+
tsIgnoreCount: 0,
|
|
193
|
+
testFiles: 0,
|
|
194
|
+
largeFiles: 0,
|
|
195
|
+
};
|
|
196
|
+
const findings = [];
|
|
197
|
+
const files = this.walkFiles(profile);
|
|
198
|
+
const maxFileBytes = profile === "full" ? 512_000 : 256_000;
|
|
199
|
+
for (const absolutePath of files) {
|
|
200
|
+
const relativePath = toPosixPath(relative(this.cwd, absolutePath));
|
|
201
|
+
const stat = statSync(absolutePath);
|
|
202
|
+
if (stat.size > maxFileBytes) {
|
|
203
|
+
counters.largeFiles += 1;
|
|
204
|
+
findings.push({
|
|
205
|
+
id: `large-file:${relativePath}`,
|
|
206
|
+
title: "Large file in scan scope",
|
|
207
|
+
severity: "medium",
|
|
208
|
+
category: "maintainability",
|
|
209
|
+
detail: `File size ${stat.size} bytes exceeds ${maxFileBytes} bytes threshold for ${profile} profile.`,
|
|
210
|
+
path: relativePath,
|
|
211
|
+
recommendation: "Split file into smaller modules or exclude it from scope if generated.",
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
let content;
|
|
216
|
+
try {
|
|
217
|
+
content = readFileSync(absolutePath, "utf8");
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (content.includes("\u0000"))
|
|
223
|
+
continue;
|
|
224
|
+
const lines = content.split(/\r?\n/);
|
|
225
|
+
counters.files += 1;
|
|
226
|
+
counters.lines += lines.length;
|
|
227
|
+
if (/(^|\/)(test|tests|__tests__)\//i.test(relativePath) || /\.test\./i.test(relativePath)) {
|
|
228
|
+
counters.testFiles += 1;
|
|
229
|
+
}
|
|
230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
231
|
+
const line = lines[i] ?? "";
|
|
232
|
+
const lineNumber = i + 1;
|
|
233
|
+
if (/(TODO|FIXME|HACK)\b/i.test(line)) {
|
|
234
|
+
counters.todoCount += 1;
|
|
235
|
+
if (findings.length < 220) {
|
|
236
|
+
findings.push({
|
|
237
|
+
id: `todo:${relativePath}:${lineNumber}`,
|
|
238
|
+
title: "Outstanding TODO/FIXME marker",
|
|
239
|
+
severity: "low",
|
|
240
|
+
category: "maintainability",
|
|
241
|
+
detail: line.trim(),
|
|
242
|
+
path: relativePath,
|
|
243
|
+
line: lineNumber,
|
|
244
|
+
recommendation: "Convert to tracked issue or resolve before release.",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (/\bconsole\.log\(|\bdebugger\b/.test(line)) {
|
|
249
|
+
counters.debugCount += 1;
|
|
250
|
+
if (findings.length < 220) {
|
|
251
|
+
findings.push({
|
|
252
|
+
id: `debug:${relativePath}:${lineNumber}`,
|
|
253
|
+
title: "Debug artifact in code path",
|
|
254
|
+
severity: "medium",
|
|
255
|
+
category: "quality",
|
|
256
|
+
detail: line.trim(),
|
|
257
|
+
path: relativePath,
|
|
258
|
+
line: lineNumber,
|
|
259
|
+
recommendation: "Remove debug call or guard behind explicit debug flag.",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (/\beval\s*\(|\bnew Function\s*\(|child_process\.(exec|execSync)\s*\(/.test(line)) {
|
|
264
|
+
counters.unsafeEvalCount += 1;
|
|
265
|
+
if (findings.length < 220) {
|
|
266
|
+
findings.push({
|
|
267
|
+
id: `unsafe:${relativePath}:${lineNumber}`,
|
|
268
|
+
title: "Potentially unsafe dynamic execution",
|
|
269
|
+
severity: "high",
|
|
270
|
+
category: "security",
|
|
271
|
+
detail: line.trim(),
|
|
272
|
+
path: relativePath,
|
|
273
|
+
line: lineNumber,
|
|
274
|
+
recommendation: "Replace with safer static alternatives or strictly sanitize inputs.",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (/:\s*any\b|<any>/.test(line)) {
|
|
279
|
+
counters.anyTypeCount += 1;
|
|
280
|
+
}
|
|
281
|
+
if (/@ts-ignore\b/.test(line)) {
|
|
282
|
+
counters.tsIgnoreCount += 1;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (counters.anyTypeCount > 15) {
|
|
287
|
+
findings.push({
|
|
288
|
+
id: "types:any-overuse",
|
|
289
|
+
title: "High usage of `any` type",
|
|
290
|
+
severity: "medium",
|
|
291
|
+
category: "type-safety",
|
|
292
|
+
detail: `Detected ${counters.anyTypeCount} occurrences of explicit any type usage.`,
|
|
293
|
+
recommendation: "Replace broad any usage with domain types or unknown + narrowing.",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (counters.tsIgnoreCount > 5) {
|
|
297
|
+
findings.push({
|
|
298
|
+
id: "types:ts-ignore-overuse",
|
|
299
|
+
title: "Excessive @ts-ignore usage",
|
|
300
|
+
severity: "medium",
|
|
301
|
+
category: "type-safety",
|
|
302
|
+
detail: `Detected ${counters.tsIgnoreCount} @ts-ignore directives.`,
|
|
303
|
+
recommendation: "Audit and remove ignored type errors; keep only documented exceptions.",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const gates = contract.quality_gates ?? [];
|
|
307
|
+
if (containsAny(gates.join(" "), ["test", "coverage"]) && counters.testFiles === 0) {
|
|
308
|
+
findings.push({
|
|
309
|
+
id: "contract:tests-missing",
|
|
310
|
+
title: "Contract gate mismatch: tests expected",
|
|
311
|
+
severity: "high",
|
|
312
|
+
category: "contract",
|
|
313
|
+
detail: "Quality gates mention tests/coverage but no test files were detected in scan scope.",
|
|
314
|
+
recommendation: "Add coverage-aligned tests or adjust contract gates before implementation.",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (containsAny(gates.join(" "), ["no todo", "todo=0", "todo:0"]) && counters.todoCount > 0) {
|
|
318
|
+
findings.push({
|
|
319
|
+
id: "contract:todo-mismatch",
|
|
320
|
+
title: "Contract gate mismatch: TODO markers present",
|
|
321
|
+
severity: "medium",
|
|
322
|
+
category: "contract",
|
|
323
|
+
detail: `Contract requires TODO cleanup but ${counters.todoCount} markers were found.`,
|
|
324
|
+
recommendation: "Resolve TODO/FIXME markers or relax gate for current cycle.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return { files, findings, counters };
|
|
328
|
+
}
|
|
329
|
+
walkFiles(profile) {
|
|
330
|
+
const maxFiles = profile === "full" ? 18_000 : 6_000;
|
|
331
|
+
const stack = [this.cwd];
|
|
332
|
+
const files = [];
|
|
333
|
+
while (stack.length > 0 && files.length < maxFiles) {
|
|
334
|
+
const dir = stack.pop();
|
|
335
|
+
if (!dir)
|
|
336
|
+
break;
|
|
337
|
+
let entries;
|
|
338
|
+
try {
|
|
339
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
const absolutePath = join(dir, entry.name);
|
|
346
|
+
if (entry.isDirectory()) {
|
|
347
|
+
if (EXCLUDED_DIR_NAMES.has(entry.name))
|
|
348
|
+
continue;
|
|
349
|
+
stack.push(absolutePath);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (!entry.isFile())
|
|
353
|
+
continue;
|
|
354
|
+
if (!SCAN_TEXT_EXTENSIONS.has(getExtension(entry.name)))
|
|
355
|
+
continue;
|
|
356
|
+
files.push(absolutePath);
|
|
357
|
+
if (files.length >= maxFiles)
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
362
|
+
}
|
|
363
|
+
buildSummary(counters, findings) {
|
|
364
|
+
const high = countSeverity(findings, "high");
|
|
365
|
+
const medium = countSeverity(findings, "medium");
|
|
366
|
+
const low = countSeverity(findings, "low");
|
|
367
|
+
const highestRisk = findings.find((item) => item.severity === "high") ?? findings[0];
|
|
368
|
+
const highestLine = highestRisk
|
|
369
|
+
? ` Highest risk: ${highestRisk.title}${highestRisk.path ? ` (${highestRisk.path})` : ""}.`
|
|
370
|
+
: "";
|
|
371
|
+
return `Scanned ${counters.files} files (${counters.lines} lines). Findings: ${high} high, ${medium} medium, ${low} low.${highestLine}`;
|
|
372
|
+
}
|
|
373
|
+
buildNextSteps(findings, contract) {
|
|
374
|
+
const steps = [];
|
|
375
|
+
const high = findings.filter((item) => item.severity === "high");
|
|
376
|
+
const medium = findings.filter((item) => item.severity === "medium");
|
|
377
|
+
if (high.length > 0) {
|
|
378
|
+
steps.push("Address all HIGH findings first and add regression checks before refactors.");
|
|
379
|
+
}
|
|
380
|
+
if (medium.length > 0) {
|
|
381
|
+
steps.push("Batch MEDIUM findings by module and apply low-blast-radius fixes incrementally.");
|
|
382
|
+
}
|
|
383
|
+
if ((contract.quality_gates ?? []).length > 0) {
|
|
384
|
+
steps.push("Re-run /blast after changes to verify contract quality gates.");
|
|
385
|
+
}
|
|
386
|
+
if (steps.length === 0) {
|
|
387
|
+
steps.push("No major risks detected; proceed with planned changes and keep /blast as pre-merge audit.");
|
|
388
|
+
}
|
|
389
|
+
return steps.slice(0, 3);
|
|
390
|
+
}
|
|
391
|
+
buildReportMarkdown(payload) {
|
|
392
|
+
const high = countSeverity(payload.findings, "high");
|
|
393
|
+
const medium = countSeverity(payload.findings, "medium");
|
|
394
|
+
const low = countSeverity(payload.findings, "low");
|
|
395
|
+
const topFindings = payload.findings
|
|
396
|
+
.sort((a, b) => {
|
|
397
|
+
const score = (severity) => (severity === "high" ? 3 : severity === "medium" ? 2 : 1);
|
|
398
|
+
return score(b.severity) - score(a.severity);
|
|
399
|
+
})
|
|
400
|
+
.slice(0, 20);
|
|
401
|
+
const contractLines = [];
|
|
402
|
+
if (payload.contract.goal)
|
|
403
|
+
contractLines.push(`- goal: ${payload.contract.goal}`);
|
|
404
|
+
if (payload.contract.quality_gates && payload.contract.quality_gates.length > 0) {
|
|
405
|
+
contractLines.push(`- quality_gates: ${payload.contract.quality_gates.join("; ")}`);
|
|
406
|
+
}
|
|
407
|
+
if (payload.contract.constraints && payload.contract.constraints.length > 0) {
|
|
408
|
+
contractLines.push(`- constraints: ${payload.contract.constraints.join("; ")}`);
|
|
409
|
+
}
|
|
410
|
+
const contractSection = contractLines.length > 0 ? contractLines.join("\n") : "- none";
|
|
411
|
+
return [
|
|
412
|
+
`# Blast Audit Report`,
|
|
413
|
+
``,
|
|
414
|
+
`- run_id: ${payload.runId}`,
|
|
415
|
+
`- profile: ${payload.profile}`,
|
|
416
|
+
`- started_at: ${payload.startedAt}`,
|
|
417
|
+
`- completed_at: ${payload.completedAt}`,
|
|
418
|
+
`- duration_ms: ${payload.durationMs}`,
|
|
419
|
+
``,
|
|
420
|
+
`## Executive Summary`,
|
|
421
|
+
`${payload.summary}`,
|
|
422
|
+
``,
|
|
423
|
+
`## Scan Metrics`,
|
|
424
|
+
`- files_scanned: ${payload.counters.files}`,
|
|
425
|
+
`- lines_scanned: ${payload.counters.lines}`,
|
|
426
|
+
`- test_files: ${payload.counters.testFiles}`,
|
|
427
|
+
`- TODO/FIXME markers: ${payload.counters.todoCount}`,
|
|
428
|
+
`- debug artifacts: ${payload.counters.debugCount}`,
|
|
429
|
+
`- unsafe dynamic execution markers: ${payload.counters.unsafeEvalCount}`,
|
|
430
|
+
`- large files skipped: ${payload.counters.largeFiles}`,
|
|
431
|
+
``,
|
|
432
|
+
`## Findings Overview`,
|
|
433
|
+
`- high: ${high}`,
|
|
434
|
+
`- medium: ${medium}`,
|
|
435
|
+
`- low: ${low}`,
|
|
436
|
+
``,
|
|
437
|
+
`## Contract Context`,
|
|
438
|
+
`${contractSection}`,
|
|
439
|
+
``,
|
|
440
|
+
`## Top Findings`,
|
|
441
|
+
topFindings.length > 0 ? topFindings.map(serializeFinding).join("\n") : "- none",
|
|
442
|
+
``,
|
|
443
|
+
`## Recommended Next Steps`,
|
|
444
|
+
payload.nextSteps.map((step, index) => `${index + 1}. ${step}`).join("\n"),
|
|
445
|
+
].join("\n");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
//# sourceMappingURL=blast.js.map
|