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,204 @@
|
|
|
1
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { analyzeWorkspace } from "../index.ts";
|
|
5
|
+
import type { Scope } from "../model/entities.ts";
|
|
6
|
+
import { runPolicy } from "../policy/policy-engine.ts";
|
|
7
|
+
import { loadPolicyFromFile } from "../policy/policy-loader.ts";
|
|
8
|
+
import type { PolicyDoc, PolicyRunResult } from "../policy/policy-types.ts";
|
|
9
|
+
import { formatPolicy } from "./format-policy.ts";
|
|
10
|
+
|
|
11
|
+
const VALID_FORMATS = new Set(["human", "json", "sarif"]);
|
|
12
|
+
|
|
13
|
+
export interface PolicyCheckOptions {
|
|
14
|
+
workspace: string;
|
|
15
|
+
policyPath?: string;
|
|
16
|
+
noPolicy?: boolean;
|
|
17
|
+
format?: "human" | "json" | "sarif";
|
|
18
|
+
out?: string;
|
|
19
|
+
deterministic?: boolean;
|
|
20
|
+
alsemVersion?: string;
|
|
21
|
+
strict?: boolean;
|
|
22
|
+
scope?: Scope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runPolicyCheck(opts: PolicyCheckOptions): Promise<number> {
|
|
26
|
+
const format = opts.format ?? "human";
|
|
27
|
+
if (!VALID_FORMATS.has(format)) {
|
|
28
|
+
process.stderr.write(`al-sem policy check: invalid --format '${format}'\n`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { model, diagnostics } = await analyzeWorkspace({ workspaceRoot: opts.workspace });
|
|
33
|
+
if (opts.strict === true && diagnostics.some((d) => d.severity === "error")) {
|
|
34
|
+
for (const d of diagnostics) process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Resolve effective policy.
|
|
39
|
+
let policy: PolicyDoc | undefined;
|
|
40
|
+
let source: string;
|
|
41
|
+
if (opts.noPolicy === true) {
|
|
42
|
+
policy = undefined;
|
|
43
|
+
source = "disabled";
|
|
44
|
+
} else if (opts.policyPath !== undefined) {
|
|
45
|
+
const abs = resolve(opts.policyPath);
|
|
46
|
+
const loaded = loadPolicyFromFile(abs);
|
|
47
|
+
if (!loaded.ok) {
|
|
48
|
+
for (const e of loaded.errors) process.stderr.write(`policy load error: ${e}\n`);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
policy = loaded.policy;
|
|
52
|
+
source = `explicit:${abs}`;
|
|
53
|
+
} else {
|
|
54
|
+
// Auto-detect workspace policy.
|
|
55
|
+
const candidates = ["al-sem.policy.yaml", "al-sem.policy.yml"];
|
|
56
|
+
let foundPath: string | undefined;
|
|
57
|
+
for (const name of candidates) {
|
|
58
|
+
const candidate = resolve(opts.workspace, name);
|
|
59
|
+
if (existsSync(candidate)) {
|
|
60
|
+
foundPath = candidate;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (foundPath !== undefined) {
|
|
65
|
+
const loaded = loadPolicyFromFile(foundPath);
|
|
66
|
+
if (!loaded.ok) {
|
|
67
|
+
for (const e of loaded.errors) process.stderr.write(`policy load error: ${e}\n`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
policy = loaded.policy;
|
|
71
|
+
source = `auto:${foundPath}`;
|
|
72
|
+
} else {
|
|
73
|
+
// Use bundled default.
|
|
74
|
+
const defaultPath = fileURLToPath(new URL("../policy/policy-default.yaml", import.meta.url));
|
|
75
|
+
if (!existsSync(defaultPath)) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`al-sem policy check: bundled default policy not found at ${defaultPath} (use --policy or --no-policy)\n`,
|
|
78
|
+
);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const loaded = loadPolicyFromFile(defaultPath);
|
|
82
|
+
if (!loaded.ok) {
|
|
83
|
+
for (const e of loaded.errors)
|
|
84
|
+
process.stderr.write(`bundled default policy load error: ${e}\n`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
policy = loaded.policy;
|
|
88
|
+
source = "default";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result: PolicyRunResult = runPolicy(model, policy, source, {
|
|
93
|
+
scope: opts.scope ?? "primary",
|
|
94
|
+
});
|
|
95
|
+
const text = formatPolicy(result, {
|
|
96
|
+
format,
|
|
97
|
+
deterministic: opts.deterministic,
|
|
98
|
+
alsemVersion: opts.alsemVersion,
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
if (opts.out !== undefined) writeFileSync(opts.out, text);
|
|
102
|
+
else process.stdout.write(text);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
process.stderr.write(`failed to write: ${(err as Error).message}\n`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
for (const d of diagnostics) process.stderr.write(`${d.severity}: ${d.message}\n`);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PolicyExplainOptions {
|
|
112
|
+
workspace: string;
|
|
113
|
+
ruleId: string;
|
|
114
|
+
policyPath?: string;
|
|
115
|
+
routine?: string;
|
|
116
|
+
findingId?: string;
|
|
117
|
+
format?: "human" | "json";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function runPolicyExplain(opts: PolicyExplainOptions): Promise<number> {
|
|
121
|
+
const format = opts.format ?? "human";
|
|
122
|
+
if (!new Set(["human", "json"]).has(format)) {
|
|
123
|
+
process.stderr.write(`al-sem policy explain: invalid --format '${format}'\n`);
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Resolve effective policy (mirrors runPolicyCheck's resolution).
|
|
128
|
+
let policy: PolicyDoc;
|
|
129
|
+
let source: string;
|
|
130
|
+
if (opts.policyPath !== undefined) {
|
|
131
|
+
const abs = resolve(opts.policyPath);
|
|
132
|
+
const loaded = loadPolicyFromFile(abs);
|
|
133
|
+
if (!loaded.ok) {
|
|
134
|
+
for (const e of loaded.errors) process.stderr.write(`policy load error: ${e}\n`);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
policy = loaded.policy;
|
|
138
|
+
source = `explicit:${abs}`;
|
|
139
|
+
} else {
|
|
140
|
+
// Auto-detect workspace policy.
|
|
141
|
+
const candidates = ["al-sem.policy.yaml", "al-sem.policy.yml"];
|
|
142
|
+
let foundPath: string | undefined;
|
|
143
|
+
for (const name of candidates) {
|
|
144
|
+
const candidate = resolve(opts.workspace, name);
|
|
145
|
+
if (existsSync(candidate)) {
|
|
146
|
+
foundPath = candidate;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (foundPath !== undefined) {
|
|
151
|
+
const loaded = loadPolicyFromFile(foundPath);
|
|
152
|
+
if (!loaded.ok) {
|
|
153
|
+
for (const e of loaded.errors) process.stderr.write(`policy load error: ${e}\n`);
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
policy = loaded.policy;
|
|
157
|
+
source = `auto:${foundPath}`;
|
|
158
|
+
} else {
|
|
159
|
+
// Use bundled default.
|
|
160
|
+
const defaultPath = fileURLToPath(new URL("../policy/policy-default.yaml", import.meta.url));
|
|
161
|
+
if (!existsSync(defaultPath)) {
|
|
162
|
+
process.stderr.write(
|
|
163
|
+
"al-sem policy explain: bundled default policy not found (use --policy)\n",
|
|
164
|
+
);
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
const loaded = loadPolicyFromFile(defaultPath);
|
|
168
|
+
if (!loaded.ok) {
|
|
169
|
+
for (const e of loaded.errors)
|
|
170
|
+
process.stderr.write(`bundled default policy load error: ${e}\n`);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
policy = loaded.policy;
|
|
174
|
+
source = "default";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const rule = policy.rules.find((r) => r.id === opts.ruleId);
|
|
179
|
+
if (rule === undefined) {
|
|
180
|
+
process.stderr.write(
|
|
181
|
+
`al-sem policy explain: rule '${opts.ruleId}' not found in effective policy (${source})\n`,
|
|
182
|
+
);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Rule-level summary output.
|
|
187
|
+
// Targeted --routine / --finding deep-trace rendering is deferred to Phase 4 follow-ups.
|
|
188
|
+
const lines: string[] = [];
|
|
189
|
+
lines.push(`Rule: ${rule.id}`);
|
|
190
|
+
if (rule.title !== undefined) lines.push(`Title: ${rule.title}`);
|
|
191
|
+
lines.push(`Severity: ${rule.severity}`);
|
|
192
|
+
lines.push(`Coverage gate: ${rule.requireCoverage ?? policy.defaults?.requireCoverage ?? "any"}`);
|
|
193
|
+
lines.push(`On unknown: ${rule.onUnknown ?? policy.defaults?.onUnknown ?? "fail-closed"}`);
|
|
194
|
+
lines.push(`Effective policy: ${source}`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("Normalized AST:");
|
|
197
|
+
lines.push(JSON.stringify(rule.when, undefined, 2));
|
|
198
|
+
if (rule.except !== undefined) {
|
|
199
|
+
lines.push("Except:");
|
|
200
|
+
lines.push(JSON.stringify(rule.except, undefined, 2));
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
5
|
+
import type { ObjectId, RoutineId } from "../model/ids.ts";
|
|
6
|
+
import { ROOT_KIND_VALUES, type RootKind } from "../model/root-classification.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Top-level shape of `roots.config.json` — workspace-rooted file that lets a
|
|
10
|
+
* developer assert routines as entry-point roots beyond what AST classification
|
|
11
|
+
* (`src/engine/root-classifier.ts`) can derive on its own. Loaded at workspace
|
|
12
|
+
* discovery time alongside `app.json`; resolution (target → routine match) and
|
|
13
|
+
* overlay merge with AST classifications happens downstream.
|
|
14
|
+
*/
|
|
15
|
+
export interface RootsConfig {
|
|
16
|
+
version: 1;
|
|
17
|
+
roots: RootsConfigEntry[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RootsConfigEntry {
|
|
21
|
+
/** Stable identifier — surfaces in diagnostics and `RootClassification.configEntryId`. */
|
|
22
|
+
id: string;
|
|
23
|
+
target: RootsConfigTarget;
|
|
24
|
+
kinds: RootKind[];
|
|
25
|
+
/** Defaults to true downstream (config entries are typically external assertions). */
|
|
26
|
+
externallyReachable?: boolean;
|
|
27
|
+
note?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RootsConfigTarget =
|
|
31
|
+
| { routineId: RoutineId }
|
|
32
|
+
| { objectId: ObjectId; routineName: string };
|
|
33
|
+
|
|
34
|
+
export interface LoadedRootsConfig {
|
|
35
|
+
/** `undefined` if the file is missing or malformed at the top level. */
|
|
36
|
+
config: RootsConfig | undefined;
|
|
37
|
+
/** sha256 of the raw file bytes; `undefined` if the file is missing. */
|
|
38
|
+
contentHash: string | undefined;
|
|
39
|
+
/** Absolute path to the loaded file; `undefined` if the file is missing. */
|
|
40
|
+
path: string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Per-entry validation diagnostics. NOT resolution errors — those belong to
|
|
43
|
+
* the overlay (Task 6).
|
|
44
|
+
*/
|
|
45
|
+
diagnostics: Diagnostic[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Set of accepted kind strings, derived from `ROOT_KIND_VALUES` so the
|
|
50
|
+
* validator's accepted set is exactly the `RootKind` union — no drift.
|
|
51
|
+
*/
|
|
52
|
+
const VALID_KINDS = new Set<string>(ROOT_KIND_VALUES);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Loads and validates `roots.config.json` from `<workspaceRoot>/roots.config.json`.
|
|
56
|
+
*
|
|
57
|
+
* Pure: never throws. All file-I/O and parse failures surface as `Diagnostic[]`
|
|
58
|
+
* (matching the engine-never-throws contract).
|
|
59
|
+
*
|
|
60
|
+
* Missing file is the common case and not an error: returns a clean empty
|
|
61
|
+
* result with no diagnostics. The returned `contentHash` is the sha256 of the
|
|
62
|
+
* raw file bytes (no whitespace normalization) so it is byte-stable for the
|
|
63
|
+
* workspaceFingerprint pipeline (Task 7).
|
|
64
|
+
*/
|
|
65
|
+
export function loadRootsConfig(workspaceRoot: string): LoadedRootsConfig {
|
|
66
|
+
const path = resolve(workspaceRoot, "roots.config.json");
|
|
67
|
+
if (!existsSync(path)) {
|
|
68
|
+
return { config: undefined, contentHash: undefined, path: undefined, diagnostics: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let bytes: Buffer;
|
|
72
|
+
try {
|
|
73
|
+
bytes = readFileSync(path);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
return {
|
|
76
|
+
config: undefined,
|
|
77
|
+
contentHash: undefined,
|
|
78
|
+
path,
|
|
79
|
+
diagnostics: [
|
|
80
|
+
diag(
|
|
81
|
+
"error",
|
|
82
|
+
`[roots-config/read-error] Cannot read roots.config.json: ${(err as Error).message}`,
|
|
83
|
+
path,
|
|
84
|
+
),
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const contentHash = createHash("sha256").update(bytes).digest("hex");
|
|
90
|
+
// `contentHash` is over the RAW bytes (BOM included) so the workspaceFingerprint
|
|
91
|
+
// pipeline sees every byte-level change. The parser input strips a leading BOM
|
|
92
|
+
// so `JSON.parse` doesn't choke on `` at position 0 — mirrors `decodeText`
|
|
93
|
+
// in `src/symbols/symbol-reference-reader.ts`.
|
|
94
|
+
const rawText = bytes.toString("utf8");
|
|
95
|
+
const text = rawText.charCodeAt(0) === 0xfeff ? rawText.slice(1) : rawText;
|
|
96
|
+
const diagnostics: Diagnostic[] = [];
|
|
97
|
+
|
|
98
|
+
let parsed: unknown;
|
|
99
|
+
try {
|
|
100
|
+
parsed = JSON.parse(text);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
diagnostics.push(diag("error", `[roots-config/parse-error] ${(err as Error).message}`, path));
|
|
103
|
+
return { config: undefined, contentHash, path, diagnostics };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const validated = validateRootsConfig(parsed, path);
|
|
107
|
+
diagnostics.push(...validated.diagnostics);
|
|
108
|
+
return { config: validated.config, contentHash, path, diagnostics };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function diag(severity: Diagnostic["severity"], message: string, sourceRef: string): Diagnostic {
|
|
112
|
+
return { severity, stage: "discover", message, sourceRef };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function validateRootsConfig(
|
|
116
|
+
parsed: unknown,
|
|
117
|
+
path: string,
|
|
118
|
+
): { config: RootsConfig | undefined; diagnostics: Diagnostic[] } {
|
|
119
|
+
const diagnostics: Diagnostic[] = [];
|
|
120
|
+
|
|
121
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
122
|
+
diagnostics.push(
|
|
123
|
+
diag(
|
|
124
|
+
"error",
|
|
125
|
+
"[roots-config/invalid-top-level] roots.config.json top level must be an object.",
|
|
126
|
+
path,
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
return { config: undefined, diagnostics };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const obj = parsed as Record<string, unknown>;
|
|
133
|
+
if (obj.version !== 1) {
|
|
134
|
+
diagnostics.push(
|
|
135
|
+
diag(
|
|
136
|
+
"error",
|
|
137
|
+
`[roots-config/unsupported-version] roots.config.json version must be 1 (got ${JSON.stringify(obj.version)}).`,
|
|
138
|
+
path,
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
return { config: undefined, diagnostics };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!Array.isArray(obj.roots)) {
|
|
145
|
+
diagnostics.push(
|
|
146
|
+
diag(
|
|
147
|
+
"error",
|
|
148
|
+
'[roots-config/invalid-roots] roots.config.json "roots" field must be an array.',
|
|
149
|
+
path,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
return { config: undefined, diagnostics };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entries: RootsConfigEntry[] = [];
|
|
156
|
+
const seenIds = new Set<string>();
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < obj.roots.length; i++) {
|
|
159
|
+
const entryUnknown = obj.roots[i];
|
|
160
|
+
if (entryUnknown === null || typeof entryUnknown !== "object" || Array.isArray(entryUnknown)) {
|
|
161
|
+
diagnostics.push(
|
|
162
|
+
diag(
|
|
163
|
+
"warning",
|
|
164
|
+
`[roots-config/invalid-root-shape] roots[${i}] is not an object; skipping.`,
|
|
165
|
+
path,
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const entry = entryUnknown as Record<string, unknown>;
|
|
171
|
+
|
|
172
|
+
if (typeof entry.id !== "string") {
|
|
173
|
+
diagnostics.push(
|
|
174
|
+
diag(
|
|
175
|
+
"warning",
|
|
176
|
+
`[roots-config/invalid-root-shape] roots[${i}] missing string "id"; skipping.`,
|
|
177
|
+
path,
|
|
178
|
+
),
|
|
179
|
+
);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (seenIds.has(entry.id)) {
|
|
184
|
+
diagnostics.push(
|
|
185
|
+
diag(
|
|
186
|
+
"warning",
|
|
187
|
+
`[roots-config/duplicate-entry-id] roots[${i}] duplicates id "${entry.id}"; skipping duplicate.`,
|
|
188
|
+
path,
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const target = parseTarget(entry.target);
|
|
195
|
+
if (target === undefined) {
|
|
196
|
+
diagnostics.push(
|
|
197
|
+
diag(
|
|
198
|
+
"warning",
|
|
199
|
+
`[roots-config/invalid-target] roots[${i}] ("${entry.id}") has invalid target; skipping.`,
|
|
200
|
+
path,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!Array.isArray(entry.kinds)) {
|
|
207
|
+
diagnostics.push(
|
|
208
|
+
diag(
|
|
209
|
+
"warning",
|
|
210
|
+
`[roots-config/invalid-root-shape] roots[${i}] ("${entry.id}") "kinds" must be an array; skipping.`,
|
|
211
|
+
path,
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Canonicalize `kinds`: dedup (silent — mirrors AST classifier behaviour) and
|
|
218
|
+
// sort in `ROOT_KIND_VALUES` declaration order. This matches the invariant
|
|
219
|
+
// documented on `RootClassification.kinds` so AST and config paths produce
|
|
220
|
+
// byte-identical kind arrays for equivalent inputs.
|
|
221
|
+
const seenKinds = new Set<RootKind>();
|
|
222
|
+
for (const k of entry.kinds) {
|
|
223
|
+
if (typeof k === "string" && VALID_KINDS.has(k)) {
|
|
224
|
+
seenKinds.add(k as RootKind);
|
|
225
|
+
} else {
|
|
226
|
+
diagnostics.push(
|
|
227
|
+
diag(
|
|
228
|
+
"warning",
|
|
229
|
+
`[roots-config/unknown-root-kind] roots[${i}] ("${entry.id}") kind ${JSON.stringify(k)} is not a known RootKind; dropping.`,
|
|
230
|
+
path,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const kinds: RootKind[] = ROOT_KIND_VALUES.filter((k) => seenKinds.has(k));
|
|
236
|
+
|
|
237
|
+
if (kinds.length === 0) {
|
|
238
|
+
diagnostics.push(
|
|
239
|
+
diag(
|
|
240
|
+
"warning",
|
|
241
|
+
`[roots-config/invalid-root-shape] roots[${i}] ("${entry.id}") has no valid kinds; skipping entry.`,
|
|
242
|
+
path,
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const validEntry: RootsConfigEntry = {
|
|
249
|
+
id: entry.id,
|
|
250
|
+
target,
|
|
251
|
+
kinds,
|
|
252
|
+
};
|
|
253
|
+
// Optional fields: present-and-correctly-typed → carry through;
|
|
254
|
+
// absent → omit; present-but-wrong-typed → drop the field AND emit a
|
|
255
|
+
// warning so authors notice typos like `externallyReachable: "true"`.
|
|
256
|
+
if (entry.externallyReachable !== undefined) {
|
|
257
|
+
if (typeof entry.externallyReachable === "boolean") {
|
|
258
|
+
validEntry.externallyReachable = entry.externallyReachable;
|
|
259
|
+
} else {
|
|
260
|
+
diagnostics.push(
|
|
261
|
+
diag(
|
|
262
|
+
"warning",
|
|
263
|
+
`[roots-config/invalid-root-shape] roots[${i}] ("${entry.id}") "externallyReachable" must be a boolean (got ${JSON.stringify(entry.externallyReachable)}); dropping field.`,
|
|
264
|
+
path,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (entry.note !== undefined) {
|
|
270
|
+
if (typeof entry.note === "string") {
|
|
271
|
+
validEntry.note = entry.note;
|
|
272
|
+
} else {
|
|
273
|
+
diagnostics.push(
|
|
274
|
+
diag(
|
|
275
|
+
"warning",
|
|
276
|
+
`[roots-config/invalid-root-shape] roots[${i}] ("${entry.id}") "note" must be a string (got ${JSON.stringify(entry.note)}); dropping field.`,
|
|
277
|
+
path,
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
entries.push(validEntry);
|
|
283
|
+
seenIds.add(entry.id);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { config: { version: 1, roots: entries }, diagnostics };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Parse a target object into one of two accepted shapes. When both `routineId`
|
|
291
|
+
* and `objectId + routineName` are present, `routineId` takes precedence
|
|
292
|
+
* (the stable id is more specific than the name-based lookup).
|
|
293
|
+
*/
|
|
294
|
+
function parseTarget(t: unknown): RootsConfigTarget | undefined {
|
|
295
|
+
if (t === null || typeof t !== "object" || Array.isArray(t)) return undefined;
|
|
296
|
+
const obj = t as Record<string, unknown>;
|
|
297
|
+
if (typeof obj.routineId === "string") return { routineId: obj.routineId };
|
|
298
|
+
if (typeof obj.objectId === "string" && typeof obj.routineName === "string") {
|
|
299
|
+
return { objectId: obj.objectId, routineName: obj.routineName };
|
|
300
|
+
}
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ANALYZER_VERSION, GRAMMAR_VERSION } from "../providers/discover.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache-affecting version stamps. Every stamp here is folded into a DependencyArtifact's
|
|
5
|
+
* cache key. Ownership (bump when…):
|
|
6
|
+
* - analyzer: release identity (already bumped per release).
|
|
7
|
+
* - grammar: tree-sitter-al grammar behaviour changes.
|
|
8
|
+
* - symbolReader: .app extraction / manifest / SymbolReference projection changes.
|
|
9
|
+
* - summarySchema: RoutineSummary shape OR semantics — lattice, residuals, transfer
|
|
10
|
+
* functions, fixed-point — changes.
|
|
11
|
+
* - depCache: artifact serialization format, canonicalization rules, or key structure
|
|
12
|
+
* changes.
|
|
13
|
+
* - resourcePolicy: the deterministic preflight-limit constants in dependency-pipeline.ts
|
|
14
|
+
* change.
|
|
15
|
+
*
|
|
16
|
+
* `symbolReader` is bumped from "1" to "2" in this phase because the .app reader was
|
|
17
|
+
* replaced (app-package-zip + the new readers).
|
|
18
|
+
*/
|
|
19
|
+
export const CACHE_VERSIONS = {
|
|
20
|
+
analyzer: ANALYZER_VERSION,
|
|
21
|
+
grammar: GRAMMAR_VERSION,
|
|
22
|
+
// symbolReader "6": Phase 0b-β — dep extraction now emits CapabilityFact[]
|
|
23
|
+
// via extractCapabilities on dep routines with embedded source. Extends
|
|
24
|
+
// RoutineSummary with capabilityFactsDirect / capabilityFactsInherited /
|
|
25
|
+
// coverage fields populated by per-family extractors + SCC composers.
|
|
26
|
+
// symbolReader "7": Phase 1 §4.3 — ObjectDecl gains objectSubtype (Codeunit
|
|
27
|
+
// Subtype property) and pageType (Page PageType property), extracted from
|
|
28
|
+
// AL source and from .app SymbolReference JSON. Required for root-classifier
|
|
29
|
+
// to identify install-codeunit / upgrade-codeunit / api-page kinds.
|
|
30
|
+
symbolReader: "7",
|
|
31
|
+
// summarySchema "6": Phase 1c — RoutineSummary's legacy boolean lattice
|
|
32
|
+
// (touchesDb, commits, writesTables, publishesEvents) deleted. The fields
|
|
33
|
+
// have been replaced by Phase 1a capability-query helpers (touchesDbOf,
|
|
34
|
+
// mayCommit, writesTablesOf, publishesEventsOf, reachableCoverage). Producer
|
|
35
|
+
// code in summary-engine.ts + summary-runner.ts simplified accordingly;
|
|
36
|
+
// fingerprint hash format changes — fixed-point convergence count may
|
|
37
|
+
// shift but final summaries are functionally equivalent.
|
|
38
|
+
// summarySchema "7": Phase 3 — IntraproceduralFeatures gained varAssignments
|
|
39
|
+
// (drives D43 IsHandled-skip detection).
|
|
40
|
+
// summarySchema "8": Phase 3.1 — IntraproceduralFeatures gained conditionReferences
|
|
41
|
+
// (drives D43 dispatch-site guard detection).
|
|
42
|
+
// summarySchema "9": capability-cone propagation — capabilityFactsInherited is now
|
|
43
|
+
// computed by bottom-up SCC-condensation cone propagation (shortest-path-wins
|
|
44
|
+
// attribution) and canonically sorted; coverage cones share the same pass. Witness/via
|
|
45
|
+
// on equal-distance ties and array order change vs the legacy per-routine BFS.
|
|
46
|
+
summarySchema: "9",
|
|
47
|
+
// depCache "2": canonical-json now omits undefined-valued object keys (was: rendered as
|
|
48
|
+
// `null` + key kept). Dependency-artifact bytes change; absent optional fields (e.g. fact
|
|
49
|
+
// resourceId) round-trip as undefined, restoring the model invariant.
|
|
50
|
+
depCache: "2",
|
|
51
|
+
resourcePolicy: "1",
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/** A stable, human-readable rendering of the version tuple — used in keys and the snapshot test. */
|
|
55
|
+
export function cacheVersionTuple(): string {
|
|
56
|
+
return [
|
|
57
|
+
`analyzer=${CACHE_VERSIONS.analyzer}`,
|
|
58
|
+
`grammar=${CACHE_VERSIONS.grammar}`,
|
|
59
|
+
`symbolReader=${CACHE_VERSIONS.symbolReader}`,
|
|
60
|
+
`summarySchema=${CACHE_VERSIONS.summarySchema}`,
|
|
61
|
+
`depCache=${CACHE_VERSIONS.depCache}`,
|
|
62
|
+
`resourcePolicy=${CACHE_VERSIONS.resourcePolicy}`,
|
|
63
|
+
].join(";");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A per-build fingerprint folded into the cache key for non-release (dev) builds, so local
|
|
68
|
+
* logic changes never silently reuse a stale artifact. Release builds (where
|
|
69
|
+
* `process.env.AL_SEM_RELEASE === "1"`) return "" — they are protected by `analyzer`.
|
|
70
|
+
*/
|
|
71
|
+
export function devFingerprint(): string {
|
|
72
|
+
if (process.env.AL_SEM_RELEASE === "1") return "";
|
|
73
|
+
return process.env.AL_SEM_DEV_FINGERPRINT ?? "dev";
|
|
74
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic JSON: object keys sorted recursively, arrays kept in order, object keys
|
|
3
|
+
* with an `undefined` value are omitted (standard JSON); `undefined` array elements render
|
|
4
|
+
* as `null`. The basis of artifact content-addressing.
|
|
5
|
+
*/
|
|
6
|
+
export function canonicalStringify(value: unknown): string {
|
|
7
|
+
if (value === undefined || value === null) return "null";
|
|
8
|
+
if (typeof value === "number") {
|
|
9
|
+
if (!Number.isFinite(value)) return "null";
|
|
10
|
+
return Object.is(value, -0) ? "0" : String(value);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
13
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
14
|
+
if (Array.isArray(value)) return `[${value.map(canonicalStringify).join(",")}]`;
|
|
15
|
+
if (typeof value === "object") {
|
|
16
|
+
const obj = value as Record<string, unknown>;
|
|
17
|
+
// Omit undefined-valued keys (standard JSON.stringify semantics). The previous behavior
|
|
18
|
+
// kept the key and rendered the value as `null`, which round-tripped absent optional
|
|
19
|
+
// fields (e.g. CapabilityFact.resourceId) back as `null` and violated the
|
|
20
|
+
// "absent ⇒ undefined" model invariant. Array undefined elements still render as `null`.
|
|
21
|
+
const keys = Object.keys(obj)
|
|
22
|
+
.filter((k) => obj[k] !== undefined)
|
|
23
|
+
.sort();
|
|
24
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalStringify(obj[k])}`).join(",")}}`;
|
|
25
|
+
}
|
|
26
|
+
return "null";
|
|
27
|
+
}
|