al-sem 0.0.1
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/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import type { AnalyzeWorkspaceResult } from "../index.ts";
|
|
2
|
+
import type { ObjectDecl, Routine, Table } from "../model/entities.ts";
|
|
3
|
+
import type { Finding } from "../model/finding.ts";
|
|
4
|
+
import type { EventSymbol } from "../model/graph.ts";
|
|
5
|
+
import type { SourceAnchor } from "../model/identity.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
|
|
8
|
+
const SEV_ORDER = ["critical", "high", "medium", "low", "info"] as const;
|
|
9
|
+
type Sev = (typeof SEV_ORDER)[number];
|
|
10
|
+
|
|
11
|
+
// Committed severity palette (OKLCH). Severity IS the data, so it carries color.
|
|
12
|
+
const SEV_COLOR: Record<Sev, string> = {
|
|
13
|
+
critical: "oklch(0.52 0.20 25)",
|
|
14
|
+
high: "oklch(0.62 0.18 45)",
|
|
15
|
+
medium: "oklch(0.74 0.14 80)",
|
|
16
|
+
low: "oklch(0.62 0.11 240)",
|
|
17
|
+
info: "oklch(0.62 0.02 255)",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Tally digit foreground per severity. Amber `medium` needs dark text for
|
|
21
|
+
// WCAG contrast; the darker fills read fine with near-white.
|
|
22
|
+
const SEV_FG: Record<Sev, string> = {
|
|
23
|
+
critical: "oklch(0.99 0 0)",
|
|
24
|
+
high: "oklch(0.99 0 0)",
|
|
25
|
+
medium: "oklch(0.26 0.04 80)",
|
|
26
|
+
low: "oklch(0.99 0 0)",
|
|
27
|
+
info: "oklch(0.99 0 0)",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CONF_COLOR: Record<string, string> = {
|
|
31
|
+
confirmed: "oklch(0.58 0.13 150)",
|
|
32
|
+
likely: "oklch(0.60 0.11 240)",
|
|
33
|
+
possible: "oklch(0.72 0.13 80)",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Fixed code-unit comparator — deterministic across locales/ICU versions,
|
|
37
|
+
// unlike String.prototype.localeCompare (byte-stable output is a contract).
|
|
38
|
+
const cmp = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
39
|
+
|
|
40
|
+
const SAFETY_RANK = { high: 3, medium: 2, low: 1 } as const;
|
|
41
|
+
|
|
42
|
+
function h(s: string): string {
|
|
43
|
+
return s
|
|
44
|
+
.replace(/&/g, "&")
|
|
45
|
+
.replace(/</g, "<")
|
|
46
|
+
.replace(/>/g, ">")
|
|
47
|
+
.replace(/"/g, """)
|
|
48
|
+
.replace(/'/g, "'");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shortFile(sourceUnitId: string): string {
|
|
52
|
+
const colon = sourceUnitId.indexOf(":");
|
|
53
|
+
return colon >= 0 ? sourceUnitId.slice(colon + 1) : sourceUnitId;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function trunc(s: string, n: number): string {
|
|
57
|
+
return s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface Maps {
|
|
61
|
+
routines: Map<string, Routine>;
|
|
62
|
+
objects: Map<string, ObjectDecl>;
|
|
63
|
+
tables: Map<string, Table>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildMaps(model: SemanticModel): Maps {
|
|
67
|
+
return {
|
|
68
|
+
routines: new Map(model.routines.map((r) => [r.id, r])),
|
|
69
|
+
objects: new Map(model.objects.map((o) => [o.id, o])),
|
|
70
|
+
tables: new Map(model.tables.map((t) => [t.id, t])),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function routineLabel(routineId: string, m: Maps): string {
|
|
75
|
+
const r = m.routines.get(routineId);
|
|
76
|
+
if (!r) return trunc(routineId, 24);
|
|
77
|
+
const o = m.objects.get(r.objectId);
|
|
78
|
+
return o ? `${o.name} :: ${r.name}` : r.name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function tableLabel(tableId: string, m: Maps): string {
|
|
82
|
+
const t = m.tables.get(tableId);
|
|
83
|
+
if (t) return t.name;
|
|
84
|
+
const parts = tableId.split("/");
|
|
85
|
+
return parts[parts.length - 1] ?? tableId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function anchorLine(a: SourceAnchor): string {
|
|
89
|
+
return `${shortFile(a.sourceUnitId)}:${a.range.startLine + 1}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// One evidence-path node. The last node is the culprit (terminal op).
|
|
93
|
+
function renderFlow(finding: Finding, m: Maps): string {
|
|
94
|
+
const steps = finding.evidencePath;
|
|
95
|
+
if (steps.length === 0) return "";
|
|
96
|
+
const nodes = steps
|
|
97
|
+
.map((step, i) => {
|
|
98
|
+
const last = i === steps.length - 1;
|
|
99
|
+
const badge = step.loopId
|
|
100
|
+
? `<span class="badge badge-loop">↻ loop</span>`
|
|
101
|
+
: step.callsiteId
|
|
102
|
+
? `<span class="badge badge-call">calls</span>`
|
|
103
|
+
: last
|
|
104
|
+
? `<span class="badge badge-op">db op</span>`
|
|
105
|
+
: "";
|
|
106
|
+
return `
|
|
107
|
+
<li class="flow-step${last ? " is-terminal" : ""}">
|
|
108
|
+
<span class="flow-rail"><span class="flow-dot"></span></span>
|
|
109
|
+
<span class="flow-body">
|
|
110
|
+
<span class="flow-head">${h(routineLabel(step.routineId, m))} ${badge}</span>
|
|
111
|
+
<span class="flow-note">${h(step.note)}</span>
|
|
112
|
+
<span class="flow-loc">${h(anchorLine(step.sourceAnchor))}</span>
|
|
113
|
+
</span>
|
|
114
|
+
</li>`;
|
|
115
|
+
})
|
|
116
|
+
.join("");
|
|
117
|
+
const extra = finding.additionalPaths?.length
|
|
118
|
+
? `<p class="flow-extra">+ ${finding.additionalPaths.length} other path${
|
|
119
|
+
finding.additionalPaths.length === 1 ? "" : "s"
|
|
120
|
+
} reach the same operation</p>`
|
|
121
|
+
: "";
|
|
122
|
+
return `<ol class="flow">${nodes}</ol>${extra}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderFinding(finding: Finding, m: Maps, coLocated: string[]): string {
|
|
126
|
+
const sev = finding.severity;
|
|
127
|
+
const conf = finding.confidence;
|
|
128
|
+
const confColor = CONF_COLOR[conf.level] ?? CONF_COLOR.possible;
|
|
129
|
+
const capped = conf.cappedBy?.length ? ` · capped by ${conf.cappedBy.join(", ")}` : "";
|
|
130
|
+
const tables = finding.affectedTables
|
|
131
|
+
.map((t) => `<span class="chip">${h(tableLabel(t, m))}</span>`)
|
|
132
|
+
.join("");
|
|
133
|
+
const fixes = [...finding.fixOptions]
|
|
134
|
+
.sort((a, b) => SAFETY_RANK[b.safety] - SAFETY_RANK[a.safety])
|
|
135
|
+
.map(
|
|
136
|
+
(f) =>
|
|
137
|
+
`<li><span class="safety safety-${f.safety}">${f.safety}</span> ${h(f.description)}</li>`,
|
|
138
|
+
)
|
|
139
|
+
.join("");
|
|
140
|
+
const co = coLocated.length
|
|
141
|
+
? `<div class="co">co-located: ${coLocated.map((d) => `<code>${h(d)}</code>`).join(" ")}</div>`
|
|
142
|
+
: "";
|
|
143
|
+
|
|
144
|
+
return `
|
|
145
|
+
<article class="finding" data-sev="${sev}" style="--sev:${SEV_COLOR[sev]}">
|
|
146
|
+
<header class="finding-head">
|
|
147
|
+
<span class="sev-dot"></span>
|
|
148
|
+
<code class="detector">${h(finding.detector)}</code>
|
|
149
|
+
<h3>${h(finding.title)}</h3>
|
|
150
|
+
<span class="conf" style="--conf:${confColor}">${h(conf.level)}${h(capped)}</span>
|
|
151
|
+
</header>
|
|
152
|
+
<p class="root-cause">${h(finding.rootCause)}</p>
|
|
153
|
+
${renderFlow(finding, m)}
|
|
154
|
+
${tables ? `<div class="tables"><span class="lbl">writes</span>${tables}</div>` : ""}
|
|
155
|
+
${fixes ? `<details class="fix"><summary>Fix options</summary><ul>${fixes}</ul></details>` : ""}
|
|
156
|
+
${co}
|
|
157
|
+
</article>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Tri-column event graph: publishers → events → subscribers.
|
|
161
|
+
function renderEventGraph(model: SemanticModel, m: Maps): string {
|
|
162
|
+
if (model.eventGraph.events.length === 0) return "";
|
|
163
|
+
const events = [...model.eventGraph.events].sort(
|
|
164
|
+
(a, b) =>
|
|
165
|
+
cmp(a.eventName, b.eventName) ||
|
|
166
|
+
cmp(a.publisherObjectId, b.publisherObjectId) ||
|
|
167
|
+
cmp(a.id, b.id),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const eventIds = new Set(events.map((e) => e.id));
|
|
171
|
+
const graphEdges = model.eventGraph.edges.filter((e) => eventIds.has(e.eventId));
|
|
172
|
+
const subscriberCount = new Set(graphEdges.map((e) => e.subscriberRoutineId)).size;
|
|
173
|
+
|
|
174
|
+
// Inline-SVG render budget. Past these, the SVG becomes an unreadable hairball;
|
|
175
|
+
// emit an explanatory note instead of silently dropping the section.
|
|
176
|
+
const MAX_EVENTS = 40;
|
|
177
|
+
const MAX_EDGES = 500;
|
|
178
|
+
const MAX_SUBSCRIBERS = 200;
|
|
179
|
+
if (
|
|
180
|
+
events.length > MAX_EVENTS ||
|
|
181
|
+
graphEdges.length > MAX_EDGES ||
|
|
182
|
+
subscriberCount > MAX_SUBSCRIBERS
|
|
183
|
+
) {
|
|
184
|
+
return `
|
|
185
|
+
<section class="graph-wrap">
|
|
186
|
+
<h2>Event graph</h2>
|
|
187
|
+
<p class="sub">Graph omitted: ${events.length} events · ${graphEdges.length} links · ${subscriberCount} subscribers exceed the inline render limit (${MAX_EVENTS}/${MAX_EDGES}/${MAX_SUBSCRIBERS}). Use <code>events fanout</code> / <code>events chains</code> for the full data.</p>
|
|
188
|
+
</section>`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const subsByEvent = new Map<string, string[]>();
|
|
192
|
+
for (const e of graphEdges) {
|
|
193
|
+
const arr = subsByEvent.get(e.eventId) ?? [];
|
|
194
|
+
arr.push(e.subscriberRoutineId);
|
|
195
|
+
subsByEvent.set(e.eventId, arr);
|
|
196
|
+
}
|
|
197
|
+
// Deterministic subscriber order per event (edge order is not output-stable).
|
|
198
|
+
for (const ids of subsByEvent.values()) {
|
|
199
|
+
ids.sort((a, b) => cmp(routineLabel(a, m), routineLabel(b, m)) || cmp(a, b));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Publisher column = the owning object (codeunit). Multiple events from one
|
|
203
|
+
// codeunit collapse to a single node, so its true fan-out is visible. Fall
|
|
204
|
+
// back to the stable object id (not a shared literal) if the object is missing.
|
|
205
|
+
const pubLabel = (ev: EventSymbol): string =>
|
|
206
|
+
m.objects.get(ev.publisherObjectId)?.name ?? ev.publisherObjectId;
|
|
207
|
+
|
|
208
|
+
// Distinct nodes per column, deterministic order.
|
|
209
|
+
const pubs = [...new Set(events.map(pubLabel))].sort(cmp);
|
|
210
|
+
const subSet = new Set<string>();
|
|
211
|
+
for (const subs of subsByEvent.values()) for (const s of subs) subSet.add(routineLabel(s, m));
|
|
212
|
+
const subsCol = [...subSet].sort(cmp);
|
|
213
|
+
|
|
214
|
+
const ROW = 46;
|
|
215
|
+
const NODE_H = 30;
|
|
216
|
+
const W = 1040;
|
|
217
|
+
const colX = { pub: 16, ev: 400, sub: 760 };
|
|
218
|
+
const colW = { pub: 250, ev: 250, sub: 264 };
|
|
219
|
+
const rows = Math.max(pubs.length, events.length, subsCol.length, 1);
|
|
220
|
+
const H = rows * ROW + 40;
|
|
221
|
+
const yOf = (i: number, count: number): number => {
|
|
222
|
+
const blockH = count * ROW;
|
|
223
|
+
const top = (H - blockH) / 2 + 20;
|
|
224
|
+
return top + i * ROW;
|
|
225
|
+
};
|
|
226
|
+
const pubY = new Map(pubs.map((p, i) => [p, yOf(i, pubs.length)]));
|
|
227
|
+
const subY = new Map(subsCol.map((s, i) => [s, yOf(i, subsCol.length)]));
|
|
228
|
+
|
|
229
|
+
const edges: string[] = [];
|
|
230
|
+
const nodes: string[] = [];
|
|
231
|
+
|
|
232
|
+
events.forEach((ev, i) => {
|
|
233
|
+
const ey = yOf(i, events.length);
|
|
234
|
+
const subs = subsByEvent.get(ev.id) ?? [];
|
|
235
|
+
const dead = subs.length === 0;
|
|
236
|
+
const evCenterY = ey + NODE_H / 2;
|
|
237
|
+
|
|
238
|
+
// publisher -> event
|
|
239
|
+
const py = (pubY.get(pubLabel(ev)) ?? ey) + NODE_H / 2;
|
|
240
|
+
edges.push(bezier(colX.pub + colW.pub, py, colX.ev, evCenterY, "oklch(0.78 0.02 255)"));
|
|
241
|
+
|
|
242
|
+
// event -> subscribers
|
|
243
|
+
for (const s of subs) {
|
|
244
|
+
const sl = routineLabel(s, m);
|
|
245
|
+
const sy = (subY.get(sl) ?? ey) + NODE_H / 2;
|
|
246
|
+
edges.push(bezier(colX.ev + colW.ev, evCenterY, colX.sub, sy, "oklch(0.7 0.08 240)"));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const fill = dead ? "oklch(0.96 0.04 25)" : "oklch(0.97 0.02 240)";
|
|
250
|
+
const stroke = dead ? SEV_COLOR.high : "oklch(0.62 0.10 240)";
|
|
251
|
+
nodes.push(
|
|
252
|
+
node(
|
|
253
|
+
colX.ev,
|
|
254
|
+
ey,
|
|
255
|
+
colW.ev,
|
|
256
|
+
NODE_H,
|
|
257
|
+
trunc(ev.eventName, 30),
|
|
258
|
+
fill,
|
|
259
|
+
stroke,
|
|
260
|
+
dead ? `${subs.length} subs` : `${subs.length}`,
|
|
261
|
+
dead,
|
|
262
|
+
ev.eventName,
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
for (const p of pubs) {
|
|
268
|
+
const y = pubY.get(p) ?? 0;
|
|
269
|
+
nodes.push(
|
|
270
|
+
node(
|
|
271
|
+
colX.pub,
|
|
272
|
+
y,
|
|
273
|
+
colW.pub,
|
|
274
|
+
NODE_H,
|
|
275
|
+
trunc(p, 30),
|
|
276
|
+
"oklch(0.98 0.005 255)",
|
|
277
|
+
"oklch(0.78 0.02 255)",
|
|
278
|
+
undefined,
|
|
279
|
+
false,
|
|
280
|
+
p,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
for (const s of subsCol) {
|
|
285
|
+
const y = subY.get(s) ?? 0;
|
|
286
|
+
nodes.push(
|
|
287
|
+
node(
|
|
288
|
+
colX.sub,
|
|
289
|
+
y,
|
|
290
|
+
colW.sub,
|
|
291
|
+
NODE_H,
|
|
292
|
+
trunc(s, 32),
|
|
293
|
+
"oklch(0.98 0.01 240)",
|
|
294
|
+
"oklch(0.7 0.08 240)",
|
|
295
|
+
undefined,
|
|
296
|
+
false,
|
|
297
|
+
s,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const headers = `
|
|
303
|
+
<text x="${colX.pub}" y="16" class="g-col">PUBLISHER</text>
|
|
304
|
+
<text x="${colX.ev}" y="16" class="g-col">EVENT</text>
|
|
305
|
+
<text x="${colX.sub}" y="16" class="g-col">SUBSCRIBER</text>`;
|
|
306
|
+
|
|
307
|
+
return `
|
|
308
|
+
<section class="graph-wrap">
|
|
309
|
+
<h2>Event graph</h2>
|
|
310
|
+
<p class="sub">Publishers fan out to subscribers across files. Events outlined in red have no subscribers (dead extension points).</p>
|
|
311
|
+
<svg viewBox="0 0 ${W} ${H}" class="evgraph" role="img" aria-label="Event publisher to subscriber graph">
|
|
312
|
+
${headers}
|
|
313
|
+
${edges.join("\n ")}
|
|
314
|
+
${nodes.join("\n ")}
|
|
315
|
+
</svg>
|
|
316
|
+
</section>`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function bezier(x1: number, y1: number, x2: number, y2: number, color: string): string {
|
|
320
|
+
const mx = (x1 + x2) / 2;
|
|
321
|
+
return `<path d="M ${x1} ${y1} C ${mx} ${y1}, ${mx} ${y2}, ${x2} ${y2}" fill="none" stroke="${color}" stroke-width="1.5" opacity="0.7"/>`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function node(
|
|
325
|
+
x: number,
|
|
326
|
+
y: number,
|
|
327
|
+
w: number,
|
|
328
|
+
hgt: number,
|
|
329
|
+
label: string,
|
|
330
|
+
fill: string,
|
|
331
|
+
stroke: string,
|
|
332
|
+
tag?: string,
|
|
333
|
+
dead?: boolean,
|
|
334
|
+
full?: string,
|
|
335
|
+
): string {
|
|
336
|
+
const tagSvg = tag
|
|
337
|
+
? `<text x="${x + w - 8}" y="${y + hgt / 2 + 4}" class="g-tag" text-anchor="end" fill="${dead ? SEV_COLOR.high : "oklch(0.55 0.02 255)"}">${h(tag)}</text>`
|
|
338
|
+
: "";
|
|
339
|
+
return `<g class="g-node"><title>${h(full ?? label)}</title><rect x="${x}" y="${y}" width="${w}" height="${hgt}" rx="7" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/><text x="${x + 12}" y="${y + hgt / 2 + 4}" class="g-label">${h(label)}</text>${tagSvg}</g>`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const STYLE = `
|
|
343
|
+
:root{
|
|
344
|
+
--bg:oklch(0.99 0.004 255);--surface:oklch(1 0 0);--border:oklch(0.91 0.008 255);
|
|
345
|
+
--ink:oklch(0.30 0.02 260);--muted:oklch(0.52 0.015 260);--accent:oklch(0.55 0.14 262);
|
|
346
|
+
--mono:"SFMono-Regular",ui-monospace,"JetBrains Mono",Menlo,Consolas,monospace;
|
|
347
|
+
--sans:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
348
|
+
}
|
|
349
|
+
*{box-sizing:border-box}
|
|
350
|
+
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);line-height:1.5;
|
|
351
|
+
-webkit-font-smoothing:antialiased}
|
|
352
|
+
.wrap{max-width:1080px;margin:0 auto;padding:48px 28px 96px}
|
|
353
|
+
.masthead{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 16px;
|
|
354
|
+
border-bottom:1px solid var(--border);padding-bottom:20px;margin-bottom:8px}
|
|
355
|
+
.masthead h1{font-size:1.7rem;font-weight:680;letter-spacing:-0.02em;margin:0}
|
|
356
|
+
.masthead h1 b{color:var(--accent)}
|
|
357
|
+
.masthead .app{font-family:var(--mono);font-size:0.85rem;color:var(--muted)}
|
|
358
|
+
.coverage{color:var(--muted);font-size:0.84rem;margin:14px 0 26px}
|
|
359
|
+
.tally{display:flex;height:34px;border-radius:8px;overflow:hidden;border:1px solid var(--border);margin-bottom:6px}
|
|
360
|
+
.tally span{display:flex;align-items:center;justify-content:center;color:oklch(0.99 0 0);
|
|
361
|
+
font-size:0.78rem;font-weight:600;min-width:34px;font-variant-numeric:tabular-nums}
|
|
362
|
+
.tally-legend{display:flex;flex-wrap:wrap;gap:14px;font-size:0.78rem;color:var(--muted);margin-bottom:40px}
|
|
363
|
+
.tally-legend i{display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:5px;vertical-align:baseline}
|
|
364
|
+
.sev-group{margin:0 0 14px}
|
|
365
|
+
.sev-group>h2{font-size:0.78rem;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted);
|
|
366
|
+
font-weight:700;margin:34px 0 12px}
|
|
367
|
+
.finding{background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
|
368
|
+
padding:18px 20px;margin:0 0 14px}
|
|
369
|
+
.finding-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
370
|
+
.sev-dot{width:11px;height:11px;border-radius:50%;background:var(--sev);flex:none;
|
|
371
|
+
box-shadow:0 0 0 4px color-mix(in oklch,var(--sev) 16%,transparent)}
|
|
372
|
+
.detector{font-family:var(--mono);font-size:0.76rem;color:var(--muted);
|
|
373
|
+
background:oklch(0.96 0.006 260);padding:2px 7px;border-radius:5px}
|
|
374
|
+
.finding-head h3{font-size:1.02rem;font-weight:620;margin:0;flex:1 1 auto;letter-spacing:-0.01em}
|
|
375
|
+
.conf{font-size:0.72rem;font-weight:600;color:var(--conf);
|
|
376
|
+
border:1px solid color-mix(in oklch,var(--conf) 40%,transparent);
|
|
377
|
+
background:color-mix(in oklch,var(--conf) 10%,transparent);padding:2px 9px;border-radius:20px;white-space:nowrap}
|
|
378
|
+
.root-cause{color:var(--ink);margin:11px 0 4px;max-width:74ch}
|
|
379
|
+
.flow{list-style:none;margin:16px 0 4px;padding:0}
|
|
380
|
+
.flow-step{display:flex;gap:14px;position:relative}
|
|
381
|
+
.flow-rail{flex:none;width:14px;display:flex;justify-content:center;position:relative}
|
|
382
|
+
.flow-rail::before{content:"";position:absolute;top:0;bottom:0;width:2px;background:var(--border)}
|
|
383
|
+
.flow-step:first-child .flow-rail::before{top:11px}
|
|
384
|
+
.flow-step:last-child .flow-rail::before{bottom:calc(100% - 11px)}
|
|
385
|
+
.flow-dot{width:11px;height:11px;border-radius:50%;background:var(--surface);
|
|
386
|
+
border:2px solid var(--muted);margin-top:5px;z-index:1}
|
|
387
|
+
.flow-step.is-terminal .flow-dot{background:var(--sev);border-color:var(--sev);
|
|
388
|
+
box-shadow:0 0 0 4px color-mix(in oklch,var(--sev) 16%,transparent)}
|
|
389
|
+
.flow-body{display:flex;flex-direction:column;padding-bottom:18px;gap:1px}
|
|
390
|
+
.flow-head{font-weight:580;font-size:0.92rem}
|
|
391
|
+
.flow-note{color:var(--muted);font-size:0.86rem}
|
|
392
|
+
.flow-loc{font-family:var(--mono);font-size:0.76rem;color:var(--accent)}
|
|
393
|
+
.flow-extra{color:var(--muted);font-size:0.8rem;margin:2px 0 0 28px}
|
|
394
|
+
.badge{font-size:0.66rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;
|
|
395
|
+
padding:1px 6px;border-radius:5px;vertical-align:middle;margin-left:4px}
|
|
396
|
+
.badge-loop{background:oklch(0.93 0.06 80);color:oklch(0.45 0.12 70)}
|
|
397
|
+
.badge-call{background:oklch(0.95 0.01 260);color:var(--muted)}
|
|
398
|
+
.badge-op{background:color-mix(in oklch,var(--sev) 18%,transparent);color:var(--sev)}
|
|
399
|
+
.tables{display:flex;align-items:center;flex-wrap:wrap;gap:6px;margin:12px 0 2px}
|
|
400
|
+
.tables .lbl{font-size:0.72rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);margin-right:2px}
|
|
401
|
+
.chip{font-family:var(--mono);font-size:0.76rem;background:oklch(0.96 0.01 260);
|
|
402
|
+
border:1px solid var(--border);padding:2px 8px;border-radius:6px}
|
|
403
|
+
.fix{margin-top:12px}
|
|
404
|
+
.fix summary{font-size:0.84rem;font-weight:600;cursor:pointer;color:var(--accent)}
|
|
405
|
+
.fix ul{margin:8px 0 0;padding-left:2px;list-style:none}
|
|
406
|
+
.fix li{margin:6px 0;font-size:0.88rem;color:var(--ink)}
|
|
407
|
+
.safety{font-size:0.66rem;font-weight:700;text-transform:uppercase;padding:1px 6px;border-radius:5px;margin-right:6px}
|
|
408
|
+
.safety-high{background:oklch(0.92 0.07 150);color:oklch(0.42 0.12 155)}
|
|
409
|
+
.safety-medium{background:oklch(0.93 0.06 80);color:oklch(0.45 0.12 70)}
|
|
410
|
+
.safety-low{background:oklch(0.93 0.05 30);color:oklch(0.48 0.14 30)}
|
|
411
|
+
.co{margin-top:11px;font-size:0.78rem;color:var(--muted)}
|
|
412
|
+
.co code{font-family:var(--mono);background:oklch(0.96 0.006 260);padding:1px 5px;border-radius:4px}
|
|
413
|
+
.graph-wrap{margin-top:56px;border-top:1px solid var(--border);padding-top:8px}
|
|
414
|
+
.graph-wrap h2{font-size:1.15rem;font-weight:640;letter-spacing:-0.01em;margin:24px 0 4px}
|
|
415
|
+
.graph-wrap .sub{color:var(--muted);font-size:0.86rem;margin:0 0 18px;max-width:70ch}
|
|
416
|
+
.evgraph{width:100%;height:auto;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:8px}
|
|
417
|
+
.g-col{font-family:var(--sans);font-size:11px;font-weight:700;letter-spacing:0.1em;fill:oklch(0.6 0.02 260)}
|
|
418
|
+
.g-label{font-family:var(--sans);font-size:12.5px;font-weight:540;fill:var(--ink)}
|
|
419
|
+
.g-tag{font-family:var(--mono);font-size:11px;font-weight:700}
|
|
420
|
+
.empty{color:var(--muted);font-style:italic;margin:40px 0}
|
|
421
|
+
.wrap footer{margin-top:56px;color:var(--muted);font-size:0.78rem;border-top:1px solid var(--border);padding-top:16px}
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
export function formatHtml(result: AnalyzeWorkspaceResult): string {
|
|
425
|
+
const m = buildMaps(result.model);
|
|
426
|
+
const findings = result.findings;
|
|
427
|
+
const cov = result.model.coverage;
|
|
428
|
+
const id = result.model.identity;
|
|
429
|
+
const app = id.primaryApp;
|
|
430
|
+
|
|
431
|
+
const counts: Record<Sev, number> = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
432
|
+
for (const f of findings) counts[f.severity]++;
|
|
433
|
+
const total = findings.length || 1;
|
|
434
|
+
|
|
435
|
+
// co-location map: file:line:col -> detectors
|
|
436
|
+
const locKey = (f: Finding): string =>
|
|
437
|
+
`${f.primaryLocation.sourceUnitId}:${f.primaryLocation.range.startLine}:${f.primaryLocation.range.startColumn}`;
|
|
438
|
+
const byLoc = new Map<string, string[]>();
|
|
439
|
+
for (const f of findings) {
|
|
440
|
+
const arr = byLoc.get(locKey(f)) ?? [];
|
|
441
|
+
arr.push(f.detector);
|
|
442
|
+
byLoc.set(locKey(f), arr);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const tally = SEV_ORDER.filter((s) => counts[s] > 0)
|
|
446
|
+
.map(
|
|
447
|
+
(s) =>
|
|
448
|
+
`<span style="flex:${counts[s]};background:${SEV_COLOR[s]};color:${SEV_FG[s]}" title="${s}">${counts[s]}</span>`,
|
|
449
|
+
)
|
|
450
|
+
.join("");
|
|
451
|
+
const legend = SEV_ORDER.map(
|
|
452
|
+
(s) => `<span><i style="background:${SEV_COLOR[s]}"></i>${s} ${counts[s]}</span>`,
|
|
453
|
+
).join("");
|
|
454
|
+
|
|
455
|
+
const groups = SEV_ORDER.map((sev) => {
|
|
456
|
+
const fs = findings.filter((f) => f.severity === sev);
|
|
457
|
+
if (fs.length === 0) return "";
|
|
458
|
+
const cards = fs
|
|
459
|
+
.map((f) => {
|
|
460
|
+
const co = (byLoc.get(locKey(f)) ?? []).filter((d) => d !== f.detector);
|
|
461
|
+
return renderFinding(f, m, [...new Set(co)]);
|
|
462
|
+
})
|
|
463
|
+
.join("");
|
|
464
|
+
return `<section class="sev-group"><h2>${sev} (${fs.length})</h2>${cards}</section>`;
|
|
465
|
+
}).join("");
|
|
466
|
+
|
|
467
|
+
const body =
|
|
468
|
+
findings.length === 0
|
|
469
|
+
? `<p class="empty">No findings. (Absence of a finding is not absence of a problem — see coverage.)</p>`
|
|
470
|
+
: groups;
|
|
471
|
+
|
|
472
|
+
const appLine = app
|
|
473
|
+
? `<span class="app">${h(app.name)} · ${h(app.version)} · ${h(app.publisher)}</span>`
|
|
474
|
+
: "";
|
|
475
|
+
|
|
476
|
+
return `<!doctype html>
|
|
477
|
+
<html lang="en">
|
|
478
|
+
<head>
|
|
479
|
+
<meta charset="utf-8">
|
|
480
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
481
|
+
<title>al-sem report${app ? ` — ${h(app.name)}` : ""}</title>
|
|
482
|
+
<style>${STYLE}</style>
|
|
483
|
+
</head>
|
|
484
|
+
<body>
|
|
485
|
+
<div class="wrap">
|
|
486
|
+
<div class="masthead">
|
|
487
|
+
<h1><b>al-sem</b> analysis report</h1>
|
|
488
|
+
${appLine}
|
|
489
|
+
</div>
|
|
490
|
+
<div class="coverage">
|
|
491
|
+
${cov.routinesTotal} routines (${cov.routinesBodyAvailable} with bodies, ${cov.routinesParseIncomplete.length} parse-incomplete) ·
|
|
492
|
+
${cov.sourceUnitsParsed}/${cov.sourceUnitsTotal} source units parsed ·
|
|
493
|
+
${cov.opaqueApps.length} opaque app(s)
|
|
494
|
+
</div>
|
|
495
|
+
<div class="tally">${tally}</div>
|
|
496
|
+
<div class="tally-legend">${legend}</div>
|
|
497
|
+
${body}
|
|
498
|
+
${renderEventGraph(result.model, m)}
|
|
499
|
+
<footer>Generated by al-sem · static semantic analysis for AL · ${findings.length} finding(s)</footer>
|
|
500
|
+
</div>
|
|
501
|
+
</body>
|
|
502
|
+
</html>`;
|
|
503
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AnalyzeWorkspaceResult } from "../index.ts";
|
|
2
|
+
|
|
3
|
+
/** Serialise the full analysis result as pretty-printed JSON.
|
|
4
|
+
* @deprecated Debug-only — can exceed 500 MB. Use `formatCompactJson` for normal output.
|
|
5
|
+
* Wire via `--dump-model` in the CLI, never as the default output.
|
|
6
|
+
*/
|
|
7
|
+
export function formatFullModelJson(result: AnalyzeWorkspaceResult): string {
|
|
8
|
+
return JSON.stringify(
|
|
9
|
+
{ model: result.model, findings: result.findings, diagnostics: result.diagnostics },
|
|
10
|
+
null,
|
|
11
|
+
2,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { PolicyRunResult } from "../policy/policy-types.ts";
|
|
2
|
+
|
|
3
|
+
export interface FormatPolicyOptions {
|
|
4
|
+
format: "human" | "json" | "sarif";
|
|
5
|
+
deterministic?: boolean;
|
|
6
|
+
alsemVersion?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function formatPolicy(result: PolicyRunResult, opts: FormatPolicyOptions): string {
|
|
10
|
+
if (opts.format === "json") {
|
|
11
|
+
return JSON.stringify(
|
|
12
|
+
{
|
|
13
|
+
al_sem_version: opts.alsemVersion ?? "0.0.0",
|
|
14
|
+
generated_at: opts.deterministic ? "0" : new Date().toISOString(),
|
|
15
|
+
kind: "policy.check",
|
|
16
|
+
policySource: result.policySource,
|
|
17
|
+
policyVersion: result.policyVersion,
|
|
18
|
+
ruleSummaries: result.ruleSummaries,
|
|
19
|
+
findings: result.findings,
|
|
20
|
+
diagnostics: result.diagnostics,
|
|
21
|
+
},
|
|
22
|
+
undefined,
|
|
23
|
+
2,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (opts.format === "sarif") {
|
|
27
|
+
return JSON.stringify(
|
|
28
|
+
{
|
|
29
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
30
|
+
version: "2.1.0",
|
|
31
|
+
runs: [
|
|
32
|
+
{
|
|
33
|
+
tool: {
|
|
34
|
+
driver: {
|
|
35
|
+
name: "al-sem",
|
|
36
|
+
version: opts.alsemVersion ?? "0.0.0",
|
|
37
|
+
rules: result.ruleSummaries.map((s) => ({
|
|
38
|
+
id: `policy-${s.ruleId}`,
|
|
39
|
+
name: s.ruleId,
|
|
40
|
+
})),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
results: result.findings.map((f) => ({
|
|
44
|
+
ruleId: f.detector,
|
|
45
|
+
message: { text: f.rootCause },
|
|
46
|
+
level:
|
|
47
|
+
f.severity === "info"
|
|
48
|
+
? "note"
|
|
49
|
+
: f.severity === "low"
|
|
50
|
+
? "note"
|
|
51
|
+
: f.severity === "medium"
|
|
52
|
+
? "warning"
|
|
53
|
+
: "error",
|
|
54
|
+
locations: [
|
|
55
|
+
{
|
|
56
|
+
physicalLocation: {
|
|
57
|
+
artifactLocation: { uri: f.primaryLocation.sourceUnitId },
|
|
58
|
+
region: {
|
|
59
|
+
startLine: f.primaryLocation.range.startLine,
|
|
60
|
+
startColumn: f.primaryLocation.range.startColumn,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
fingerprints:
|
|
66
|
+
f.fingerprint !== undefined ? { "al-sem/v1": f.fingerprint } : undefined,
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
undefined,
|
|
72
|
+
2,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
// human
|
|
76
|
+
const lines: string[] = [];
|
|
77
|
+
lines.push(`Policy check — ${result.policySource}`);
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push("Rule summaries:");
|
|
80
|
+
for (const s of result.ruleSummaries) {
|
|
81
|
+
lines.push(
|
|
82
|
+
` ${s.ruleId}: evaluated=${s.routinesEvaluated} matched=${s.routinesMatched} passed=${s.routinesPassed} skipped(coverage)=${s.routinesSkippedCoverage} skipped(unknown)=${s.routinesSkippedUnknown} findings=${s.findingsEmitted}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
lines.push("");
|
|
86
|
+
if (result.findings.length === 0) {
|
|
87
|
+
lines.push("No policy findings.");
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(`Findings (${result.findings.length}):`);
|
|
90
|
+
for (const f of result.findings) {
|
|
91
|
+
lines.push(` [${f.severity}] ${f.detector} — ${f.title ?? f.rootCause}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `${lines.join("\n")}\n`;
|
|
95
|
+
}
|