snipara-companion 3.6.0 → 3.6.2
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 +19 -0
- package/dist/index.d.ts +58 -1
- package/dist/index.js +475 -13
- package/docs/CODE_GRAPH.md +74 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
Release notes for `snipara-companion`, newest first.
|
|
4
4
|
|
|
5
|
+
## New In 3.6.2
|
|
6
|
+
|
|
7
|
+
- Adds `snipara.semantic.v1` to local code overlays and impact results with
|
|
8
|
+
evidence-backed public APIs, implicit contracts, architecture roles, and
|
|
9
|
+
critical-versus-incidental dependency classification.
|
|
10
|
+
- Marks explicit TypeScript and Go exports during extraction and bumps the
|
|
11
|
+
local extractor version to `3` while preserving the overlay v2 envelope.
|
|
12
|
+
- Adds semantic risk points to local impact, preserving every contributing
|
|
13
|
+
assertion and rule as inspectable evidence.
|
|
14
|
+
- Merges local and hosted semantic assertions with explicit provenance in
|
|
15
|
+
hybrid mode. Historical regression-path associations stay shadow-only and
|
|
16
|
+
cannot silently change the risk score.
|
|
17
|
+
|
|
18
|
+
## New In 3.6.1
|
|
19
|
+
|
|
20
|
+
- Enforces `--max-nodes` as a strict total traversal budget, including seed
|
|
21
|
+
symbols, so impact analysis cannot exceed the configured graph bound for
|
|
22
|
+
symbol-heavy files.
|
|
23
|
+
|
|
5
24
|
## New In 3.6.0
|
|
6
25
|
|
|
7
26
|
- Upgrades the local code overlay to v2 with TypeScript Compiler API symbols,
|
package/dist/index.d.ts
CHANGED
|
@@ -1808,6 +1808,61 @@ interface LocalCodeOverlaySymbol {
|
|
|
1808
1808
|
endLine?: number;
|
|
1809
1809
|
qualifiedName: string;
|
|
1810
1810
|
localKey: string;
|
|
1811
|
+
exported?: boolean;
|
|
1812
|
+
defaultExport?: boolean;
|
|
1813
|
+
}
|
|
1814
|
+
type LocalSemanticPredicate = "public_api" | "implicit_contract" | "architecture_role" | "dependency_criticality";
|
|
1815
|
+
interface LocalSemanticEvidence {
|
|
1816
|
+
kind: string;
|
|
1817
|
+
detail: string;
|
|
1818
|
+
filePath?: string;
|
|
1819
|
+
line?: number;
|
|
1820
|
+
edgeKind?: LocalCodeOverlayEdge["kind"];
|
|
1821
|
+
}
|
|
1822
|
+
interface LocalSemanticAssertion {
|
|
1823
|
+
id: string;
|
|
1824
|
+
subject: string;
|
|
1825
|
+
predicate: LocalSemanticPredicate;
|
|
1826
|
+
value: string | boolean;
|
|
1827
|
+
confidence: number;
|
|
1828
|
+
source: "typescript-compiler-api" | "language-parser" | "architecture-pattern-rules" | "criticality-rules" | "graph-criticality-rules";
|
|
1829
|
+
extractorVersion: 1;
|
|
1830
|
+
evidence: LocalSemanticEvidence[];
|
|
1831
|
+
}
|
|
1832
|
+
interface LocalSemanticModel {
|
|
1833
|
+
version: "snipara.semantic.v1";
|
|
1834
|
+
extractorVersion: 1;
|
|
1835
|
+
source: "deterministic-graph-inference";
|
|
1836
|
+
scope: "repository" | "impact";
|
|
1837
|
+
assertions: LocalSemanticAssertion[];
|
|
1838
|
+
publicContracts: LocalSemanticAssertion[];
|
|
1839
|
+
architectureRoles: LocalSemanticAssertion[];
|
|
1840
|
+
dependencyCriticality: LocalSemanticAssertion[];
|
|
1841
|
+
truncation: {
|
|
1842
|
+
truncated: boolean;
|
|
1843
|
+
totalAssertionCount: number;
|
|
1844
|
+
returnedAssertionCount: number;
|
|
1845
|
+
assertionLimit: number;
|
|
1846
|
+
};
|
|
1847
|
+
historicalRegression: {
|
|
1848
|
+
mode: "shadow";
|
|
1849
|
+
sampleThreshold: 3;
|
|
1850
|
+
failureEventCount: 0;
|
|
1851
|
+
associations: [];
|
|
1852
|
+
riskContributionEnabled: false;
|
|
1853
|
+
suggestedRiskDelta: 0;
|
|
1854
|
+
caveat: string;
|
|
1855
|
+
};
|
|
1856
|
+
summary: {
|
|
1857
|
+
assertionCount: number;
|
|
1858
|
+
publicContractCount: number;
|
|
1859
|
+
architectureRoleCount: number;
|
|
1860
|
+
criticalDependencyCount: number;
|
|
1861
|
+
criticalEdgeCount: number;
|
|
1862
|
+
incidentalDependencyCount: number;
|
|
1863
|
+
historicalAssociationCount: 0;
|
|
1864
|
+
};
|
|
1865
|
+
caveats: string[];
|
|
1811
1866
|
}
|
|
1812
1867
|
interface LocalCodeOverlayImportBinding {
|
|
1813
1868
|
localName: string;
|
|
@@ -1854,7 +1909,7 @@ interface LocalCodeOverlayExcludedFile {
|
|
|
1854
1909
|
}
|
|
1855
1910
|
interface LocalCodeOverlayManifest {
|
|
1856
1911
|
version: "snipara.local_code_overlay.v2";
|
|
1857
|
-
extractorVersion: 2;
|
|
1912
|
+
extractorVersion: 2 | 3;
|
|
1858
1913
|
generatedAt: string;
|
|
1859
1914
|
indexedAt: string;
|
|
1860
1915
|
repoRoot: string;
|
|
@@ -1874,6 +1929,7 @@ interface LocalCodeOverlayManifest {
|
|
|
1874
1929
|
imports: LocalCodeOverlayImport[];
|
|
1875
1930
|
references: LocalCodeOverlayReference[];
|
|
1876
1931
|
edges: LocalCodeOverlayEdge[];
|
|
1932
|
+
semantic?: LocalSemanticModel;
|
|
1877
1933
|
incremental: {
|
|
1878
1934
|
reusedFiles: number;
|
|
1879
1935
|
parsedFiles: number;
|
|
@@ -2010,6 +2066,7 @@ interface LocalCodeOverlaySummary {
|
|
|
2010
2066
|
imports: number;
|
|
2011
2067
|
references: number;
|
|
2012
2068
|
edges: number;
|
|
2069
|
+
semanticAssertions: number;
|
|
2013
2070
|
excluded: number;
|
|
2014
2071
|
};
|
|
2015
2072
|
incremental: LocalCodeOverlayManifest["incremental"];
|
package/dist/index.js
CHANGED
|
@@ -3440,6 +3440,17 @@ function typeScriptLine(sourceFile, node) {
|
|
|
3440
3440
|
function typeScriptEndLine(sourceFile, node) {
|
|
3441
3441
|
return sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
3442
3442
|
}
|
|
3443
|
+
function typeScriptExportStatus(node, name, namedExports) {
|
|
3444
|
+
let declaration = node;
|
|
3445
|
+
if (import_typescript.default.isVariableDeclaration(node) && import_typescript.default.isVariableDeclarationList(node.parent)) {
|
|
3446
|
+
const statement = node.parent.parent;
|
|
3447
|
+
if (import_typescript.default.isVariableStatement(statement)) declaration = statement;
|
|
3448
|
+
}
|
|
3449
|
+
const modifiers = import_typescript.default.canHaveModifiers(declaration) ? import_typescript.default.getModifiers(declaration) : void 0;
|
|
3450
|
+
const exported = namedExports.has(name) || modifiers?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.ExportKeyword);
|
|
3451
|
+
const defaultExport = modifiers?.some((modifier) => modifier.kind === import_typescript.default.SyntaxKind.DefaultKeyword) ?? false;
|
|
3452
|
+
return { exported: Boolean(exported || defaultExport), defaultExport };
|
|
3453
|
+
}
|
|
3443
3454
|
function extractTypeScript(content, filePath) {
|
|
3444
3455
|
const symbols = [];
|
|
3445
3456
|
const imports = [];
|
|
@@ -3452,9 +3463,18 @@ function extractTypeScript(content, filePath) {
|
|
|
3452
3463
|
true,
|
|
3453
3464
|
typeScriptScriptKind(filePath)
|
|
3454
3465
|
);
|
|
3466
|
+
const namedExports = /* @__PURE__ */ new Set();
|
|
3467
|
+
for (const statement of sourceFile.statements) {
|
|
3468
|
+
if (import_typescript.default.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && import_typescript.default.isNamedExports(statement.exportClause)) {
|
|
3469
|
+
for (const element of statement.exportClause.elements) {
|
|
3470
|
+
namedExports.add(element.propertyName?.text ?? element.name.text);
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3455
3474
|
const addSymbol = (node, name, kind, ownerQualifiedName, ownerKey) => {
|
|
3456
3475
|
const qualifiedName = ownerQualifiedName ? `${ownerQualifiedName}.${name}` : name;
|
|
3457
3476
|
const line = typeScriptLine(sourceFile, node);
|
|
3477
|
+
const exportStatus = ownerQualifiedName ? { exported: false, defaultExport: false } : typeScriptExportStatus(node, name, namedExports);
|
|
3458
3478
|
const symbol = {
|
|
3459
3479
|
name,
|
|
3460
3480
|
kind,
|
|
@@ -3462,7 +3482,9 @@ function extractTypeScript(content, filePath) {
|
|
|
3462
3482
|
line,
|
|
3463
3483
|
endLine: typeScriptEndLine(sourceFile, node),
|
|
3464
3484
|
qualifiedName,
|
|
3465
|
-
localKey: buildSymbolKey(filePath, qualifiedName, kind)
|
|
3485
|
+
localKey: buildSymbolKey(filePath, qualifiedName, kind),
|
|
3486
|
+
exported: exportStatus.exported,
|
|
3487
|
+
defaultExport: exportStatus.defaultExport
|
|
3466
3488
|
};
|
|
3467
3489
|
symbols.push(symbol);
|
|
3468
3490
|
if (ownerKey) {
|
|
@@ -3604,7 +3626,9 @@ function extractPython(content, filePath) {
|
|
|
3604
3626
|
filePath,
|
|
3605
3627
|
line,
|
|
3606
3628
|
qualifiedName,
|
|
3607
|
-
localKey: buildSymbolKey(filePath, qualifiedName, kind)
|
|
3629
|
+
localKey: buildSymbolKey(filePath, qualifiedName, kind),
|
|
3630
|
+
exported: /^[A-Z]/.test(name),
|
|
3631
|
+
defaultExport: false
|
|
3608
3632
|
});
|
|
3609
3633
|
}
|
|
3610
3634
|
for (const match of content.matchAll(importPattern)) {
|
|
@@ -3771,7 +3795,7 @@ function buildLocalCodeOverlay(options = {}) {
|
|
|
3771
3795
|
}
|
|
3772
3796
|
const contentHash2 = sha2563(indexedContent);
|
|
3773
3797
|
const previousFile = previousFiles.get(filePath);
|
|
3774
|
-
const canReuse = previous?.version === "snipara.local_code_overlay.v2" && previous.extractorVersion ===
|
|
3798
|
+
const canReuse = previous?.version === "snipara.local_code_overlay.v2" && previous.extractorVersion === 3 && previousFile?.sha256 === contentHash2 && previousFile.language === language;
|
|
3775
3799
|
const extracted = canReuse ? {
|
|
3776
3800
|
symbols: previous.symbols.filter((item) => item.filePath === filePath),
|
|
3777
3801
|
imports: previous.imports.filter((item) => item.filePath === filePath),
|
|
@@ -3798,6 +3822,7 @@ function buildLocalCodeOverlay(options = {}) {
|
|
|
3798
3822
|
extractedEdges.push(...extracted.edges);
|
|
3799
3823
|
}
|
|
3800
3824
|
const edges = resolveLocalGraphEdges(files, symbols, imports, references2, extractedEdges);
|
|
3825
|
+
const semantic = buildLocalSemanticModel({ symbols, edges });
|
|
3801
3826
|
for (const file of files) {
|
|
3802
3827
|
file.edgeCount = edges.filter((edge) => edge.filePath === file.path).length;
|
|
3803
3828
|
}
|
|
@@ -3833,7 +3858,7 @@ function buildLocalCodeOverlay(options = {}) {
|
|
|
3833
3858
|
) : null;
|
|
3834
3859
|
return {
|
|
3835
3860
|
version: "snipara.local_code_overlay.v2",
|
|
3836
|
-
extractorVersion:
|
|
3861
|
+
extractorVersion: 3,
|
|
3837
3862
|
generatedAt: now,
|
|
3838
3863
|
indexedAt: now,
|
|
3839
3864
|
repoRoot,
|
|
@@ -3859,6 +3884,7 @@ function buildLocalCodeOverlay(options = {}) {
|
|
|
3859
3884
|
imports,
|
|
3860
3885
|
references: references2,
|
|
3861
3886
|
edges,
|
|
3887
|
+
semantic,
|
|
3862
3888
|
incremental: {
|
|
3863
3889
|
reusedFiles,
|
|
3864
3890
|
parsedFiles,
|
|
@@ -3895,6 +3921,7 @@ function summarizeLocalCodeOverlay(manifest) {
|
|
|
3895
3921
|
imports: manifest.imports.length,
|
|
3896
3922
|
references: manifest.references.length,
|
|
3897
3923
|
edges: manifest.edges.length,
|
|
3924
|
+
semanticAssertions: manifest.semantic?.assertions.length ?? 0,
|
|
3898
3925
|
excluded: manifest.excluded.total
|
|
3899
3926
|
},
|
|
3900
3927
|
incremental: manifest.incremental,
|
|
@@ -4226,9 +4253,10 @@ function traverseLocalGraph(manifest, seeds, options) {
|
|
|
4226
4253
|
incoming.set(edge.to, [...incoming.get(edge.to) ?? [], edge]);
|
|
4227
4254
|
outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge]);
|
|
4228
4255
|
}
|
|
4229
|
-
const
|
|
4230
|
-
const
|
|
4231
|
-
const
|
|
4256
|
+
const uniqueSeeds = [...new Set(seeds)];
|
|
4257
|
+
const boundedSeeds = uniqueSeeds.slice(0, options.maxNodes);
|
|
4258
|
+
const visited = new Set(boundedSeeds);
|
|
4259
|
+
const queue = boundedSeeds.map((key) => ({
|
|
4232
4260
|
key,
|
|
4233
4261
|
depth: 0,
|
|
4234
4262
|
path: [key],
|
|
@@ -4236,7 +4264,7 @@ function traverseLocalGraph(manifest, seeds, options) {
|
|
|
4236
4264
|
}));
|
|
4237
4265
|
const nodes = [];
|
|
4238
4266
|
const traversedEdges = [];
|
|
4239
|
-
let truncated =
|
|
4267
|
+
let truncated = boundedSeeds.length < uniqueSeeds.length;
|
|
4240
4268
|
while (queue.length > 0) {
|
|
4241
4269
|
const current = queue.shift();
|
|
4242
4270
|
if (!current || current.depth >= options.depth) continue;
|
|
@@ -4246,7 +4274,7 @@ function traverseLocalGraph(manifest, seeds, options) {
|
|
|
4246
4274
|
];
|
|
4247
4275
|
for (const candidate of candidates) {
|
|
4248
4276
|
if (visited.has(candidate.next)) continue;
|
|
4249
|
-
if (visited.size >= options.maxNodes
|
|
4277
|
+
if (visited.size >= options.maxNodes) {
|
|
4250
4278
|
truncated = true;
|
|
4251
4279
|
queue.length = 0;
|
|
4252
4280
|
break;
|
|
@@ -4280,6 +4308,366 @@ function uniqueTraversalFiles(nodes, excluded) {
|
|
|
4280
4308
|
)
|
|
4281
4309
|
].sort();
|
|
4282
4310
|
}
|
|
4311
|
+
var LOCAL_SEMANTIC_TEST_PATH = /(^|\/)(tests?|__tests__|fixtures?)(\/|$)|\.(test|spec)\./i;
|
|
4312
|
+
var LOCAL_SEMANTIC_SENSITIVE_PATH = /(^|\/)(auth|middleware|billing|payments?|stripe|webhooks?|prisma|schema|migrations?|deploy)(\/|\.|$)/i;
|
|
4313
|
+
var LOCAL_SEMANTIC_CONTRACT_PATH = /(^|\/)(schemas?|contracts?|protocols?|interfaces?|config|tool_defs)(\/|\.|$)|route\.(ts|tsx|py)$/i;
|
|
4314
|
+
function localSemanticEvidence(kind, detail, metadata = {}) {
|
|
4315
|
+
return { kind, detail, ...metadata };
|
|
4316
|
+
}
|
|
4317
|
+
function localSemanticAssertion(subject, predicate, value, confidence, source2, evidence) {
|
|
4318
|
+
const identity = JSON.stringify([subject, predicate, value, source2]);
|
|
4319
|
+
return {
|
|
4320
|
+
id: `semantic-${sha2563(identity).slice(0, 16)}`,
|
|
4321
|
+
subject,
|
|
4322
|
+
predicate,
|
|
4323
|
+
value,
|
|
4324
|
+
confidence: Math.round(Math.max(0, Math.min(1, confidence)) * 1e3) / 1e3,
|
|
4325
|
+
source: source2,
|
|
4326
|
+
extractorVersion: 1,
|
|
4327
|
+
evidence
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
function localArchitectureRoles(symbol) {
|
|
4331
|
+
const haystack = `${symbol.filePath} ${symbol.name}`;
|
|
4332
|
+
const candidates = [
|
|
4333
|
+
{ role: "repository", pattern: /repository|(^|[_.])repo($|[_.])/i, confidence: 0.9 },
|
|
4334
|
+
{ role: "adapter", pattern: /adapter/i, confidence: 0.9 },
|
|
4335
|
+
{ role: "facade", pattern: /facade/i, confidence: 0.9 },
|
|
4336
|
+
{ role: "controller", pattern: /controller/i, confidence: 0.85 },
|
|
4337
|
+
{ role: "service", pattern: /service/i, confidence: 0.78 },
|
|
4338
|
+
{ role: "worker", pattern: /worker|background[_-]?job|indexer/i, confidence: 0.82 },
|
|
4339
|
+
{ role: "factory", pattern: /factory|builder/i, confidence: 0.76 }
|
|
4340
|
+
];
|
|
4341
|
+
const roles = [];
|
|
4342
|
+
if (/\/app\/api\//i.test(symbol.filePath) || /\/route\.(ts|tsx|py)$/i.test(symbol.filePath)) {
|
|
4343
|
+
roles.push(
|
|
4344
|
+
localSemanticAssertion(
|
|
4345
|
+
symbol.localKey,
|
|
4346
|
+
"architecture_role",
|
|
4347
|
+
"route_handler",
|
|
4348
|
+
0.94,
|
|
4349
|
+
"architecture-pattern-rules",
|
|
4350
|
+
[
|
|
4351
|
+
localSemanticEvidence("route_path", "Route path identifies an API boundary.", {
|
|
4352
|
+
filePath: symbol.filePath,
|
|
4353
|
+
line: symbol.line
|
|
4354
|
+
})
|
|
4355
|
+
]
|
|
4356
|
+
)
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
if (LOCAL_SEMANTIC_TEST_PATH.test(symbol.filePath)) {
|
|
4360
|
+
roles.push(
|
|
4361
|
+
localSemanticAssertion(
|
|
4362
|
+
symbol.localKey,
|
|
4363
|
+
"architecture_role",
|
|
4364
|
+
"test",
|
|
4365
|
+
0.98,
|
|
4366
|
+
"architecture-pattern-rules",
|
|
4367
|
+
[localSemanticEvidence("test_path", "File is under a test surface.")]
|
|
4368
|
+
)
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
4371
|
+
for (const candidate of candidates) {
|
|
4372
|
+
if (!candidate.pattern.test(haystack)) continue;
|
|
4373
|
+
roles.push(
|
|
4374
|
+
localSemanticAssertion(
|
|
4375
|
+
symbol.localKey,
|
|
4376
|
+
"architecture_role",
|
|
4377
|
+
candidate.role,
|
|
4378
|
+
candidate.confidence,
|
|
4379
|
+
"architecture-pattern-rules",
|
|
4380
|
+
[
|
|
4381
|
+
localSemanticEvidence(
|
|
4382
|
+
"name_or_path_pattern",
|
|
4383
|
+
`Name or path matches the ${candidate.role} pattern.`,
|
|
4384
|
+
{ filePath: symbol.filePath, line: symbol.line }
|
|
4385
|
+
)
|
|
4386
|
+
]
|
|
4387
|
+
)
|
|
4388
|
+
);
|
|
4389
|
+
}
|
|
4390
|
+
return roles;
|
|
4391
|
+
}
|
|
4392
|
+
function localSymbolSemanticAssertions(symbol) {
|
|
4393
|
+
const assertions = [];
|
|
4394
|
+
const routeSurface = /\/app\/api\//i.test(symbol.filePath) || /\/route\.(ts|tsx|py)$/i.test(symbol.filePath);
|
|
4395
|
+
const toolSurface = /(^|\/)(mcp|tool_defs)(\/|\.|$)/i.test(symbol.filePath);
|
|
4396
|
+
const publicEvidence = [];
|
|
4397
|
+
if (symbol.exported) {
|
|
4398
|
+
publicEvidence.push(
|
|
4399
|
+
localSemanticEvidence(
|
|
4400
|
+
"explicit_export",
|
|
4401
|
+
"TypeScript/Go declaration is explicitly exported.",
|
|
4402
|
+
{
|
|
4403
|
+
filePath: symbol.filePath,
|
|
4404
|
+
line: symbol.line
|
|
4405
|
+
}
|
|
4406
|
+
)
|
|
4407
|
+
);
|
|
4408
|
+
}
|
|
4409
|
+
if (routeSurface) {
|
|
4410
|
+
publicEvidence.push(
|
|
4411
|
+
localSemanticEvidence("route_path", "Symbol is declared in a route module.", {
|
|
4412
|
+
filePath: symbol.filePath,
|
|
4413
|
+
line: symbol.line
|
|
4414
|
+
})
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
if (toolSurface) {
|
|
4418
|
+
publicEvidence.push(
|
|
4419
|
+
localSemanticEvidence(
|
|
4420
|
+
"tool_surface",
|
|
4421
|
+
"Symbol is declared in an MCP/tool-definition surface.",
|
|
4422
|
+
{
|
|
4423
|
+
filePath: symbol.filePath,
|
|
4424
|
+
line: symbol.line
|
|
4425
|
+
}
|
|
4426
|
+
)
|
|
4427
|
+
);
|
|
4428
|
+
}
|
|
4429
|
+
if (publicEvidence.length > 0) {
|
|
4430
|
+
assertions.push(
|
|
4431
|
+
localSemanticAssertion(
|
|
4432
|
+
symbol.localKey,
|
|
4433
|
+
"public_api",
|
|
4434
|
+
true,
|
|
4435
|
+
Math.min(0.98, 0.82 + publicEvidence.length * 0.05),
|
|
4436
|
+
symbol.exported ? "typescript-compiler-api" : "architecture-pattern-rules",
|
|
4437
|
+
publicEvidence
|
|
4438
|
+
)
|
|
4439
|
+
);
|
|
4440
|
+
}
|
|
4441
|
+
let contractKind = null;
|
|
4442
|
+
const contractEvidence = [];
|
|
4443
|
+
if (symbol.exported && ["interface", "type"].includes(symbol.kind)) {
|
|
4444
|
+
contractKind = "exported_type_contract";
|
|
4445
|
+
contractEvidence.push(
|
|
4446
|
+
localSemanticEvidence("symbol_kind", `Exported ${symbol.kind} defines a structural contract.`)
|
|
4447
|
+
);
|
|
4448
|
+
} else if (LOCAL_SEMANTIC_CONTRACT_PATH.test(symbol.filePath)) {
|
|
4449
|
+
contractKind = "repository_boundary_contract";
|
|
4450
|
+
contractEvidence.push(
|
|
4451
|
+
localSemanticEvidence(
|
|
4452
|
+
"contract_path",
|
|
4453
|
+
"File path denotes a schema, contract, config, route, or tool boundary."
|
|
4454
|
+
)
|
|
4455
|
+
);
|
|
4456
|
+
} else if (symbol.exported) {
|
|
4457
|
+
contractKind = "public_surface_contract";
|
|
4458
|
+
contractEvidence.push(...publicEvidence.slice(0, 1));
|
|
4459
|
+
}
|
|
4460
|
+
if (contractKind) {
|
|
4461
|
+
assertions.push(
|
|
4462
|
+
localSemanticAssertion(
|
|
4463
|
+
symbol.localKey,
|
|
4464
|
+
"implicit_contract",
|
|
4465
|
+
contractKind,
|
|
4466
|
+
["interface", "type"].includes(symbol.kind) ? 0.92 : 0.78,
|
|
4467
|
+
symbol.exported ? "typescript-compiler-api" : "architecture-pattern-rules",
|
|
4468
|
+
contractEvidence
|
|
4469
|
+
)
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
assertions.push(...localArchitectureRoles(symbol));
|
|
4473
|
+
let criticality = "ordinary";
|
|
4474
|
+
let criticalityConfidence = 0.58;
|
|
4475
|
+
let criticalityEvidence = [
|
|
4476
|
+
localSemanticEvidence("default", "No critical or incidental evidence matched.")
|
|
4477
|
+
];
|
|
4478
|
+
if (LOCAL_SEMANTIC_TEST_PATH.test(symbol.filePath)) {
|
|
4479
|
+
criticality = "incidental";
|
|
4480
|
+
criticalityConfidence = 0.95;
|
|
4481
|
+
criticalityEvidence = [localSemanticEvidence("test_path", "Test-only dependency.")];
|
|
4482
|
+
} else if (LOCAL_SEMANTIC_SENSITIVE_PATH.test(symbol.filePath) || routeSurface || toolSurface) {
|
|
4483
|
+
criticality = "critical";
|
|
4484
|
+
criticalityConfidence = 0.92;
|
|
4485
|
+
criticalityEvidence = [
|
|
4486
|
+
localSemanticEvidence(
|
|
4487
|
+
"sensitive_surface",
|
|
4488
|
+
"Path identifies an auth, billing, schema, webhook, deploy, route, or MCP surface."
|
|
4489
|
+
)
|
|
4490
|
+
];
|
|
4491
|
+
} else if (publicEvidence.length > 0) {
|
|
4492
|
+
criticality = "important";
|
|
4493
|
+
criticalityConfidence = 0.85;
|
|
4494
|
+
criticalityEvidence = [
|
|
4495
|
+
localSemanticEvidence(
|
|
4496
|
+
"exported_surface",
|
|
4497
|
+
"Explicit export defines a module contract but is not by itself a critical runtime surface."
|
|
4498
|
+
)
|
|
4499
|
+
];
|
|
4500
|
+
} else if (contractKind) {
|
|
4501
|
+
criticality = "important";
|
|
4502
|
+
criticalityConfidence = 0.82;
|
|
4503
|
+
criticalityEvidence = [
|
|
4504
|
+
localSemanticEvidence("contract_surface", "Dependency defines a repository contract.")
|
|
4505
|
+
];
|
|
4506
|
+
}
|
|
4507
|
+
assertions.push(
|
|
4508
|
+
localSemanticAssertion(
|
|
4509
|
+
symbol.localKey,
|
|
4510
|
+
"dependency_criticality",
|
|
4511
|
+
criticality,
|
|
4512
|
+
criticalityConfidence,
|
|
4513
|
+
"criticality-rules",
|
|
4514
|
+
criticalityEvidence
|
|
4515
|
+
)
|
|
4516
|
+
);
|
|
4517
|
+
return assertions;
|
|
4518
|
+
}
|
|
4519
|
+
function buildLocalSemanticModel(manifest, options = {}) {
|
|
4520
|
+
const symbols = options.symbolKeys ? manifest.symbols.filter((symbol) => options.symbolKeys?.has(symbol.localKey)) : manifest.symbols;
|
|
4521
|
+
const assertions = symbols.flatMap(localSymbolSemanticAssertions);
|
|
4522
|
+
const symbolByKey = new Map(manifest.symbols.map((symbol) => [symbol.localKey, symbol]));
|
|
4523
|
+
const criticalityBySymbol = new Map(
|
|
4524
|
+
assertions.filter((assertion) => assertion.predicate === "dependency_criticality").map((assertion) => [assertion.subject, String(assertion.value)])
|
|
4525
|
+
);
|
|
4526
|
+
const criticalityRank = {
|
|
4527
|
+
incidental: 0,
|
|
4528
|
+
ordinary: 1,
|
|
4529
|
+
important: 2,
|
|
4530
|
+
critical: 3
|
|
4531
|
+
};
|
|
4532
|
+
const criticalityByFile = /* @__PURE__ */ new Map();
|
|
4533
|
+
for (const symbol of manifest.symbols) {
|
|
4534
|
+
const value = criticalityBySymbol.get(symbol.localKey);
|
|
4535
|
+
if (!value) continue;
|
|
4536
|
+
const current = criticalityByFile.get(symbol.filePath);
|
|
4537
|
+
if (!current || criticalityRank[value] > criticalityRank[current]) {
|
|
4538
|
+
criticalityByFile.set(symbol.filePath, value);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
for (const edge of options.edges ?? []) {
|
|
4542
|
+
const fromSymbol = symbolByKey.get(edge.from);
|
|
4543
|
+
const toSymbol = symbolByKey.get(edge.to);
|
|
4544
|
+
const fromFile = fromSymbol?.filePath ?? edge.from.replace(/^local-file::/, "");
|
|
4545
|
+
const toFile = toSymbol?.filePath ?? edge.to.replace(/^local-file::/, "");
|
|
4546
|
+
const fromCriticality = criticalityBySymbol.get(edge.from) ?? criticalityByFile.get(fromFile);
|
|
4547
|
+
const toCriticality = criticalityBySymbol.get(edge.to) ?? criticalityByFile.get(toFile);
|
|
4548
|
+
const edgeSubject = `edge::${edge.from}::${edge.kind}::${edge.to}`;
|
|
4549
|
+
const evidence = [
|
|
4550
|
+
localSemanticEvidence("graph_edge", `${edge.kind} from ${fromFile} to ${toFile}.`, {
|
|
4551
|
+
filePath: edge.filePath,
|
|
4552
|
+
line: edge.line,
|
|
4553
|
+
edgeKind: edge.kind
|
|
4554
|
+
})
|
|
4555
|
+
];
|
|
4556
|
+
let value = "ordinary";
|
|
4557
|
+
let confidence = 0.62;
|
|
4558
|
+
if (LOCAL_SEMANTIC_TEST_PATH.test(fromFile) || LOCAL_SEMANTIC_TEST_PATH.test(toFile)) {
|
|
4559
|
+
value = "incidental";
|
|
4560
|
+
confidence = 0.92;
|
|
4561
|
+
evidence.push(localSemanticEvidence("test_path", "At least one edge endpoint is test-only."));
|
|
4562
|
+
} else if (fromCriticality === "critical" || toCriticality === "critical") {
|
|
4563
|
+
value = "critical";
|
|
4564
|
+
confidence = 0.9;
|
|
4565
|
+
evidence.push(
|
|
4566
|
+
localSemanticEvidence("critical_endpoint", "At least one dependency endpoint is critical.")
|
|
4567
|
+
);
|
|
4568
|
+
} else if (edge.kind === "CONTAINS" || edge.confidence < 0.65) {
|
|
4569
|
+
value = "incidental";
|
|
4570
|
+
confidence = 0.78;
|
|
4571
|
+
evidence.push(
|
|
4572
|
+
localSemanticEvidence(
|
|
4573
|
+
"weak_or_structural",
|
|
4574
|
+
"Containment or low-confidence evidence is incidental to change propagation."
|
|
4575
|
+
)
|
|
4576
|
+
);
|
|
4577
|
+
} else if (fromCriticality === "important" || toCriticality === "important") {
|
|
4578
|
+
value = "important";
|
|
4579
|
+
confidence = 0.82;
|
|
4580
|
+
evidence.push(
|
|
4581
|
+
localSemanticEvidence(
|
|
4582
|
+
"contract_endpoint",
|
|
4583
|
+
"At least one dependency endpoint is a contract."
|
|
4584
|
+
)
|
|
4585
|
+
);
|
|
4586
|
+
}
|
|
4587
|
+
assertions.push(
|
|
4588
|
+
localSemanticAssertion(
|
|
4589
|
+
edgeSubject,
|
|
4590
|
+
"dependency_criticality",
|
|
4591
|
+
value,
|
|
4592
|
+
confidence,
|
|
4593
|
+
"graph-criticality-rules",
|
|
4594
|
+
evidence
|
|
4595
|
+
)
|
|
4596
|
+
);
|
|
4597
|
+
}
|
|
4598
|
+
const allAssertions = [
|
|
4599
|
+
...new Map(assertions.map((assertion) => [assertion.id, assertion])).values()
|
|
4600
|
+
].sort(
|
|
4601
|
+
(left, right) => `${left.subject}:${left.predicate}:${String(left.value)}`.localeCompare(
|
|
4602
|
+
`${right.subject}:${right.predicate}:${String(right.value)}`
|
|
4603
|
+
)
|
|
4604
|
+
);
|
|
4605
|
+
const assertionLimit = (options.scope ?? "repository") === "impact" ? 2e3 : 5e3;
|
|
4606
|
+
const returnedAssertions = allAssertions.slice(0, assertionLimit);
|
|
4607
|
+
const allPublicContracts = allAssertions.filter(
|
|
4608
|
+
(assertion) => assertion.predicate === "public_api" || assertion.predicate === "implicit_contract"
|
|
4609
|
+
);
|
|
4610
|
+
const allArchitectureRoles = allAssertions.filter(
|
|
4611
|
+
(assertion) => assertion.predicate === "architecture_role"
|
|
4612
|
+
);
|
|
4613
|
+
const allDependencyCriticality = allAssertions.filter(
|
|
4614
|
+
(assertion) => assertion.predicate === "dependency_criticality"
|
|
4615
|
+
);
|
|
4616
|
+
const publicContracts = returnedAssertions.filter(
|
|
4617
|
+
(assertion) => assertion.predicate === "public_api" || assertion.predicate === "implicit_contract"
|
|
4618
|
+
);
|
|
4619
|
+
const architectureRoles = returnedAssertions.filter(
|
|
4620
|
+
(assertion) => assertion.predicate === "architecture_role"
|
|
4621
|
+
);
|
|
4622
|
+
const dependencyCriticality = returnedAssertions.filter(
|
|
4623
|
+
(assertion) => assertion.predicate === "dependency_criticality"
|
|
4624
|
+
);
|
|
4625
|
+
return {
|
|
4626
|
+
version: "snipara.semantic.v1",
|
|
4627
|
+
extractorVersion: 1,
|
|
4628
|
+
source: "deterministic-graph-inference",
|
|
4629
|
+
scope: options.scope ?? "repository",
|
|
4630
|
+
assertions: returnedAssertions,
|
|
4631
|
+
publicContracts,
|
|
4632
|
+
architectureRoles,
|
|
4633
|
+
dependencyCriticality,
|
|
4634
|
+
truncation: {
|
|
4635
|
+
truncated: allAssertions.length > assertionLimit,
|
|
4636
|
+
totalAssertionCount: allAssertions.length,
|
|
4637
|
+
returnedAssertionCount: returnedAssertions.length,
|
|
4638
|
+
assertionLimit
|
|
4639
|
+
},
|
|
4640
|
+
historicalRegression: {
|
|
4641
|
+
mode: "shadow",
|
|
4642
|
+
sampleThreshold: 3,
|
|
4643
|
+
failureEventCount: 0,
|
|
4644
|
+
associations: [],
|
|
4645
|
+
riskContributionEnabled: false,
|
|
4646
|
+
suggestedRiskDelta: 0,
|
|
4647
|
+
caveat: "The local overlay has no durable regression outcome stream. Historical path learning stays shadow-only until hosted evidence is available."
|
|
4648
|
+
},
|
|
4649
|
+
summary: {
|
|
4650
|
+
assertionCount: allAssertions.length,
|
|
4651
|
+
publicContractCount: allPublicContracts.length,
|
|
4652
|
+
architectureRoleCount: allArchitectureRoles.length,
|
|
4653
|
+
criticalDependencyCount: allDependencyCriticality.filter(
|
|
4654
|
+
(assertion) => assertion.value === "critical"
|
|
4655
|
+
).length,
|
|
4656
|
+
criticalEdgeCount: allDependencyCriticality.filter(
|
|
4657
|
+
(assertion) => assertion.value === "critical" && assertion.subject.startsWith("edge::")
|
|
4658
|
+
).length,
|
|
4659
|
+
incidentalDependencyCount: allDependencyCriticality.filter(
|
|
4660
|
+
(assertion) => assertion.value === "incidental"
|
|
4661
|
+
).length,
|
|
4662
|
+
historicalAssociationCount: 0
|
|
4663
|
+
},
|
|
4664
|
+
caveats: [
|
|
4665
|
+
"Semantic assertions are deterministic inferences, not declarations of author intent.",
|
|
4666
|
+
"Name-only architecture patterns use lower confidence than explicit export or path evidence.",
|
|
4667
|
+
"Historical regression learning requires hosted outcome evidence and remains shadow-only."
|
|
4668
|
+
]
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4283
4671
|
function loadLocalRiskConfig(repoRoot) {
|
|
4284
4672
|
const defaults = {
|
|
4285
4673
|
depthDecay: 0.65,
|
|
@@ -4304,7 +4692,7 @@ function loadLocalRiskConfig(repoRoot) {
|
|
|
4304
4692
|
return defaults;
|
|
4305
4693
|
}
|
|
4306
4694
|
}
|
|
4307
|
-
function buildLocalRisk(manifest, changedFiles, traversal, depth) {
|
|
4695
|
+
function buildLocalRisk(manifest, changedFiles, traversal, depth, semantic) {
|
|
4308
4696
|
const config = loadLocalRiskConfig(manifest.repoRoot);
|
|
4309
4697
|
const allFiles = [
|
|
4310
4698
|
...changedFiles,
|
|
@@ -4343,13 +4731,27 @@ function buildLocalRisk(manifest, changedFiles, traversal, depth) {
|
|
|
4343
4731
|
(reference) => changedFiles.includes(reference.filePath) && !resolvedReferenceKeys.has(`${reference.from}:${reference.line}`)
|
|
4344
4732
|
).length;
|
|
4345
4733
|
const uncertaintyPenalty = Math.min(15, unresolvedReferences * 2 + (traversal.truncated ? 8 : 0));
|
|
4346
|
-
const
|
|
4734
|
+
const criticalSemanticDependencies = semantic.summary.criticalEdgeCount;
|
|
4735
|
+
const changedSymbolKeys = new Set(
|
|
4736
|
+
manifest.symbols.filter((symbol) => changedFiles.includes(symbol.filePath)).map((symbol) => symbol.localKey)
|
|
4737
|
+
);
|
|
4738
|
+
const changedContracts = manifest.symbols.filter((symbol) => changedSymbolKeys.has(symbol.localKey)).flatMap(localSymbolSemanticAssertions).filter(
|
|
4739
|
+
(assertion) => assertion.predicate === "public_api" || assertion.predicate === "implicit_contract"
|
|
4740
|
+
).length;
|
|
4741
|
+
const semanticRiskPoints = Math.min(20, criticalSemanticDependencies * 3) + Math.min(10, changedContracts * 2);
|
|
4742
|
+
const rawScore = criticality * (directFiles.size * 8 + propagation * 4) + uncertaintyPenalty + semanticRiskPoints;
|
|
4347
4743
|
const score = Math.max(0, Math.min(100, Math.round(rawScore)));
|
|
4348
4744
|
const reasons = [
|
|
4349
4745
|
`${directFiles.size} direct dependent file${directFiles.size === 1 ? "" : "s"}`,
|
|
4350
4746
|
`${transitiveFiles.size} transitive dependent file${transitiveFiles.size === 1 ? "" : "s"}`,
|
|
4351
4747
|
...criticalMatches.length ? [`critical-surface multiplier ${criticality.toFixed(2)}`] : [],
|
|
4352
4748
|
...unresolvedReferences ? [`${unresolvedReferences} unresolved reference${unresolvedReferences === 1 ? "" : "s"}`] : [],
|
|
4749
|
+
...criticalSemanticDependencies ? [
|
|
4750
|
+
`${criticalSemanticDependencies} critical semantic dependenc${criticalSemanticDependencies === 1 ? "y" : "ies"}`
|
|
4751
|
+
] : [],
|
|
4752
|
+
...changedContracts ? [
|
|
4753
|
+
`${changedContracts} public or implicit contract assertion${changedContracts === 1 ? "" : "s"}`
|
|
4754
|
+
] : [],
|
|
4353
4755
|
...traversal.truncated ? ["traversal truncated by max-nodes"] : []
|
|
4354
4756
|
];
|
|
4355
4757
|
return {
|
|
@@ -4360,9 +4762,12 @@ function buildLocalRisk(manifest, changedFiles, traversal, depth) {
|
|
|
4360
4762
|
transitiveDependentFiles: transitiveFiles.size,
|
|
4361
4763
|
criticality,
|
|
4362
4764
|
uncertaintyPenalty,
|
|
4765
|
+
semanticRiskPoints,
|
|
4766
|
+
criticalSemanticDependencies,
|
|
4767
|
+
changedContracts,
|
|
4363
4768
|
unresolvedReferences,
|
|
4364
4769
|
reasons,
|
|
4365
|
-
formula: "criticality * (directImpact + decayedEdgePropagation) + uncertaintyPenalty"
|
|
4770
|
+
formula: "criticality * (directImpact + decayedEdgePropagation) + uncertaintyPenalty + semanticRiskPoints"
|
|
4366
4771
|
};
|
|
4367
4772
|
}
|
|
4368
4773
|
function missingTargetDetail(manifest, filePath) {
|
|
@@ -4678,6 +5083,42 @@ function hybridRisk(local, hosted) {
|
|
|
4678
5083
|
)
|
|
4679
5084
|
};
|
|
4680
5085
|
}
|
|
5086
|
+
function hybridSemantic(local, hosted) {
|
|
5087
|
+
const localSemantic = asUnknownRecord(local.semantic);
|
|
5088
|
+
const hostedSemantic = asUnknownRecord(hosted.semantic);
|
|
5089
|
+
if (Object.keys(localSemantic).length === 0 && Object.keys(hostedSemantic).length === 0) {
|
|
5090
|
+
return void 0;
|
|
5091
|
+
}
|
|
5092
|
+
const assertions = mergeResultArrays(
|
|
5093
|
+
[{ assertions: hostedSemantic.assertions }, { assertions: localSemantic.assertions }],
|
|
5094
|
+
["assertions"]
|
|
5095
|
+
);
|
|
5096
|
+
return {
|
|
5097
|
+
version: "snipara.semantic.hybrid.v1",
|
|
5098
|
+
mode: "hybrid",
|
|
5099
|
+
provenance: {
|
|
5100
|
+
canonicalBase: Object.keys(hostedSemantic).length > 0 ? "hosted_graph" : "unavailable",
|
|
5101
|
+
checkoutDelta: Object.keys(localSemantic).length > 0 ? "local_overlay" : "unavailable",
|
|
5102
|
+
crossSourceAssertionSynthesis: false
|
|
5103
|
+
},
|
|
5104
|
+
assertions,
|
|
5105
|
+
summary: {
|
|
5106
|
+
assertionCount: assertions.length,
|
|
5107
|
+
publicContractCount: assertions.filter(
|
|
5108
|
+
(item) => asUnknownRecord(item).predicate === "public_api" || asUnknownRecord(item).predicate === "implicit_contract"
|
|
5109
|
+
).length,
|
|
5110
|
+
architectureRoleCount: assertions.filter(
|
|
5111
|
+
(item) => asUnknownRecord(item).predicate === "architecture_role"
|
|
5112
|
+
).length,
|
|
5113
|
+
criticalDependencyCount: assertions.filter(
|
|
5114
|
+
(item) => asUnknownRecord(item).predicate === "dependency_criticality" && asUnknownRecord(item).value === "critical"
|
|
5115
|
+
).length
|
|
5116
|
+
},
|
|
5117
|
+
historicalRegression: hostedSemantic.historical_regression ?? hostedSemantic.historicalRegression ?? localSemantic.historicalRegression ?? null,
|
|
5118
|
+
local: Object.keys(localSemantic).length > 0 ? localSemantic : null,
|
|
5119
|
+
hosted: Object.keys(hostedSemantic).length > 0 ? hostedSemantic : null
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
4681
5122
|
function mergeHybridCodeResults(verb, localResult, hostedResult, hostedError) {
|
|
4682
5123
|
const local = asUnknownRecord(localResult);
|
|
4683
5124
|
const hosted = asUnknownRecord(hostedResult);
|
|
@@ -4697,6 +5138,7 @@ function mergeHybridCodeResults(verb, localResult, hostedResult, hostedError) {
|
|
|
4697
5138
|
collectStringValues(local, /* @__PURE__ */ new Set(["changed_files", "changedFiles"]), changedFiles);
|
|
4698
5139
|
collectStringValues(hosted, /* @__PURE__ */ new Set(["changed_files", "changedFiles"]), changedFiles);
|
|
4699
5140
|
const risk = hybridRisk(local, hosted);
|
|
5141
|
+
const semantic = hybridSemantic(local, hosted);
|
|
4700
5142
|
const recommendedTests = mergeResultArrays(
|
|
4701
5143
|
[hosted, local],
|
|
4702
5144
|
["recommended_tests", "recommendedTests", "related_tests", "relatedTests", "tests"]
|
|
@@ -4724,6 +5166,7 @@ function mergeHybridCodeResults(verb, localResult, hostedResult, hostedError) {
|
|
|
4724
5166
|
changed_files: [...changedFiles].sort(),
|
|
4725
5167
|
impactedFiles: [...affectedFiles].sort(),
|
|
4726
5168
|
...risk ? { risk } : {},
|
|
5169
|
+
...semantic ? { semantic } : {},
|
|
4727
5170
|
recommended_tests: recommendedTests,
|
|
4728
5171
|
recommended_actions: recommendedActions,
|
|
4729
5172
|
coverage_gaps: coverageGaps,
|
|
@@ -5341,6 +5784,24 @@ function buildLocalImpactResult(options) {
|
|
|
5341
5784
|
edgeKinds: options.edgeKinds?.length ? options.edgeKinds : ["IMPORTS", "CALLS", "REFERENCES"],
|
|
5342
5785
|
maxNodes
|
|
5343
5786
|
});
|
|
5787
|
+
const traversalEdges = [
|
|
5788
|
+
...new Map(
|
|
5789
|
+
traversal.nodes.flatMap((node) => node.edges).map((edge) => [`${edge.from}:${edge.kind}:${edge.to}:${edge.line}`, edge])
|
|
5790
|
+
).values()
|
|
5791
|
+
];
|
|
5792
|
+
const semanticSymbolKeys = /* @__PURE__ */ new Set([...seedKeys, ...traversal.nodes.map((node) => node.key)]);
|
|
5793
|
+
const semanticFiles = /* @__PURE__ */ new Set([
|
|
5794
|
+
...changedFiles,
|
|
5795
|
+
...traversal.nodes.flatMap((node) => node.filePath ? [node.filePath] : [])
|
|
5796
|
+
]);
|
|
5797
|
+
for (const candidate of manifest.symbols) {
|
|
5798
|
+
if (semanticFiles.has(candidate.filePath)) semanticSymbolKeys.add(candidate.localKey);
|
|
5799
|
+
}
|
|
5800
|
+
const semantic = buildLocalSemanticModel(manifest, {
|
|
5801
|
+
scope: "impact",
|
|
5802
|
+
symbolKeys: semanticSymbolKeys,
|
|
5803
|
+
edges: traversalEdges
|
|
5804
|
+
});
|
|
5344
5805
|
const transitiveFiles = uniqueTraversalFiles(traversal.nodes, selectedFiles);
|
|
5345
5806
|
const impactedFiles = [.../* @__PURE__ */ new Set([...directImpactedFiles, ...transitiveFiles])].sort();
|
|
5346
5807
|
const relevantWarningFiles = /* @__PURE__ */ new Set([
|
|
@@ -5381,7 +5842,8 @@ function buildLocalImpactResult(options) {
|
|
|
5381
5842
|
visitedCount: traversal.visitedCount,
|
|
5382
5843
|
maxNodes
|
|
5383
5844
|
},
|
|
5384
|
-
|
|
5845
|
+
semantic,
|
|
5846
|
+
risk: buildLocalRisk(manifest, changedFiles, traversal, depth, semantic)
|
|
5385
5847
|
};
|
|
5386
5848
|
}
|
|
5387
5849
|
async function codeLocalImportsCommand(options) {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Semantic Code Graph
|
|
2
|
+
|
|
3
|
+
`snipara-companion code impact` builds a structural graph from the current
|
|
4
|
+
checkout and adds a deterministic semantic model above it. The semantic layer
|
|
5
|
+
does not replace `CALLS`, `REFERENCES`, `IMPORTS`, or `CONTAINS` edges. It
|
|
6
|
+
classifies what those nodes and edges mean for a change.
|
|
7
|
+
|
|
8
|
+
## Semantic model v1
|
|
9
|
+
|
|
10
|
+
Local manifests and impact results expose `snipara.semantic.v1`. Each assertion
|
|
11
|
+
contains:
|
|
12
|
+
|
|
13
|
+
- `subject`: a stable local symbol key or edge identity;
|
|
14
|
+
- `predicate`: `public_api`, `implicit_contract`, `architecture_role`, or
|
|
15
|
+
`dependency_criticality`;
|
|
16
|
+
- `value`: the inferred classification;
|
|
17
|
+
- `confidence`: a bounded score from `0` to `1`;
|
|
18
|
+
- `source` and `extractorVersion`;
|
|
19
|
+
- `evidence`: the export modifier, symbol kind, file/path pattern, or graph edge
|
|
20
|
+
that produced the assertion.
|
|
21
|
+
|
|
22
|
+
The extractor recognizes explicit TypeScript and Go exports, route and MCP
|
|
23
|
+
surfaces, exported type contracts, and common Repository, Adapter, Facade,
|
|
24
|
+
Controller, Service, Worker, Factory, schema, route-handler, and test roles.
|
|
25
|
+
Name-only patterns have lower confidence than explicit export, route, or graph
|
|
26
|
+
evidence.
|
|
27
|
+
|
|
28
|
+
## Dependency criticality
|
|
29
|
+
|
|
30
|
+
Criticality is evaluated for symbols and for the edges traversed by impact:
|
|
31
|
+
|
|
32
|
+
- `critical`: runtime-exposed route/MCP surfaces and sensitive auth, billing,
|
|
33
|
+
schema, webhook, or deploy paths;
|
|
34
|
+
- `important`: explicit module exports and inferred contract boundaries;
|
|
35
|
+
- `ordinary`: normal application dependencies without stronger evidence;
|
|
36
|
+
- `incidental`: test-only, containment, or weak-confidence dependencies.
|
|
37
|
+
|
|
38
|
+
Local risk scoring adds bounded `semanticRiskPoints` for critical traversed
|
|
39
|
+
dependencies and contracts in the changed files. The output lists the exact
|
|
40
|
+
assertions and the formula; callers never receive a hidden semantic multiplier.
|
|
41
|
+
Large impact traversals cap returned assertions at 2,000 (repository manifests
|
|
42
|
+
at 5,000) and report total/returned counts in `semantic.truncation`; risk uses
|
|
43
|
+
the complete pre-truncation counts.
|
|
44
|
+
|
|
45
|
+
## Hybrid provenance
|
|
46
|
+
|
|
47
|
+
Hybrid queries preserve hosted canonical assertions and local checkout-delta
|
|
48
|
+
assertions under `snipara.semantic.hybrid.v1`. Snipara unions the evidence but
|
|
49
|
+
does not manufacture cross-source edges or assertions.
|
|
50
|
+
|
|
51
|
+
## Historical regression paths
|
|
52
|
+
|
|
53
|
+
Hosted impact can associate sanitized warning/error execution traces with
|
|
54
|
+
graph paths by their tracked files. Version 1 reports sample count, confidence,
|
|
55
|
+
event IDs, and a suggested risk delta. This surface is intentionally
|
|
56
|
+
`mode: shadow` and `riskContributionEnabled: false`: temporal association is
|
|
57
|
+
not causal proof, and it cannot affect risk until separately calibrated and
|
|
58
|
+
promoted.
|
|
59
|
+
|
|
60
|
+
The local overlay has no durable outcome stream, so it returns the same shadow
|
|
61
|
+
contract with zero samples and directs callers to hosted evidence when needed.
|
|
62
|
+
|
|
63
|
+
## Example
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx -y snipara-companion code impact \
|
|
67
|
+
--source local \
|
|
68
|
+
--changed-files src/auth/session-repository.ts \
|
|
69
|
+
--depth 4 \
|
|
70
|
+
--json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Inspect `result.semantic.assertions`, `result.semantic.summary`, and
|
|
74
|
+
`result.risk.semanticRiskPoints` in the response.
|