snipara-companion 3.6.1 → 3.6.3
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 +24 -0
- package/dist/index.d.ts +83 -1
- package/dist/index.js +649 -8
- package/docs/CODE_GRAPH.md +106 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
Release notes for `snipara-companion`, newest first.
|
|
4
4
|
|
|
5
|
+
## New In 3.6.3
|
|
6
|
+
|
|
7
|
+
- Labels every semantic `confidence` value as an uncalibrated
|
|
8
|
+
`heuristic_prior`, preserving the numeric compatibility field without
|
|
9
|
+
presenting it as a measured probability.
|
|
10
|
+
- Loads bounded, literal project naming terms from
|
|
11
|
+
`.snipara/semantic-rules.json`; teams can extend or replace English defaults
|
|
12
|
+
without enabling arbitrary regular expressions.
|
|
13
|
+
- Keeps historical regression associations strictly shadow-only and marks
|
|
14
|
+
their association strength as uncalibrated.
|
|
15
|
+
|
|
16
|
+
## New In 3.6.2
|
|
17
|
+
|
|
18
|
+
- Adds `snipara.semantic.v1` to local code overlays and impact results with
|
|
19
|
+
evidence-backed public APIs, implicit contracts, architecture roles, and
|
|
20
|
+
critical-versus-incidental dependency classification.
|
|
21
|
+
- Marks explicit TypeScript and Go exports during extraction and bumps the
|
|
22
|
+
local extractor version to `3` while preserving the overlay v2 envelope.
|
|
23
|
+
- Adds semantic risk points to local impact, preserving every contributing
|
|
24
|
+
assertion and rule as inspectable evidence.
|
|
25
|
+
- Merges local and hosted semantic assertions with explicit provenance in
|
|
26
|
+
hybrid mode. Historical regression-path associations stay shadow-only and
|
|
27
|
+
cannot silently change the risk score.
|
|
28
|
+
|
|
5
29
|
## New In 3.6.1
|
|
6
30
|
|
|
7
31
|
- Enforces `--max-nodes` as a strict total traversal budget, including seed
|
package/dist/index.d.ts
CHANGED
|
@@ -1808,6 +1808,86 @@ 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
|
+
scoreKind: "heuristic_prior";
|
|
1829
|
+
calibrated: false;
|
|
1830
|
+
source: "typescript-compiler-api" | "language-parser" | "architecture-pattern-rules" | "criticality-rules" | "graph-criticality-rules";
|
|
1831
|
+
extractorVersion: 1;
|
|
1832
|
+
evidence: LocalSemanticEvidence[];
|
|
1833
|
+
}
|
|
1834
|
+
interface LocalSemanticModel {
|
|
1835
|
+
version: "snipara.semantic.v1";
|
|
1836
|
+
extractorVersion: 1;
|
|
1837
|
+
source: "deterministic-graph-inference";
|
|
1838
|
+
modelType: "rule-based-heuristic";
|
|
1839
|
+
scoreContract: {
|
|
1840
|
+
kind: "heuristic_prior";
|
|
1841
|
+
calibrated: false;
|
|
1842
|
+
probability: false;
|
|
1843
|
+
comparableAcrossProjects: false;
|
|
1844
|
+
basis: "hand-tuned-v1";
|
|
1845
|
+
rulesetVersion: 1;
|
|
1846
|
+
interpretation: string;
|
|
1847
|
+
};
|
|
1848
|
+
ruleConfig: {
|
|
1849
|
+
version: "snipara.semantic.rules.v1";
|
|
1850
|
+
source: "defaults" | "project-file";
|
|
1851
|
+
path: ".snipara/semantic-rules.json";
|
|
1852
|
+
replaceDefaults: boolean;
|
|
1853
|
+
sensitivePathTermCount: number;
|
|
1854
|
+
contractPathTermCount: number;
|
|
1855
|
+
testPathTermCount: number;
|
|
1856
|
+
architectureRoleTermCount: number;
|
|
1857
|
+
configuredRoles: string[];
|
|
1858
|
+
configHash: string;
|
|
1859
|
+
warnings: string[];
|
|
1860
|
+
};
|
|
1861
|
+
scope: "repository" | "impact";
|
|
1862
|
+
assertions: LocalSemanticAssertion[];
|
|
1863
|
+
publicContracts: LocalSemanticAssertion[];
|
|
1864
|
+
architectureRoles: LocalSemanticAssertion[];
|
|
1865
|
+
dependencyCriticality: LocalSemanticAssertion[];
|
|
1866
|
+
truncation: {
|
|
1867
|
+
truncated: boolean;
|
|
1868
|
+
totalAssertionCount: number;
|
|
1869
|
+
returnedAssertionCount: number;
|
|
1870
|
+
assertionLimit: number;
|
|
1871
|
+
};
|
|
1872
|
+
historicalRegression: {
|
|
1873
|
+
mode: "shadow";
|
|
1874
|
+
sampleThreshold: 3;
|
|
1875
|
+
failureEventCount: 0;
|
|
1876
|
+
associations: [];
|
|
1877
|
+
riskContributionEnabled: false;
|
|
1878
|
+
suggestedRiskDelta: 0;
|
|
1879
|
+
caveat: string;
|
|
1880
|
+
};
|
|
1881
|
+
summary: {
|
|
1882
|
+
assertionCount: number;
|
|
1883
|
+
publicContractCount: number;
|
|
1884
|
+
architectureRoleCount: number;
|
|
1885
|
+
criticalDependencyCount: number;
|
|
1886
|
+
criticalEdgeCount: number;
|
|
1887
|
+
incidentalDependencyCount: number;
|
|
1888
|
+
historicalAssociationCount: 0;
|
|
1889
|
+
};
|
|
1890
|
+
caveats: string[];
|
|
1811
1891
|
}
|
|
1812
1892
|
interface LocalCodeOverlayImportBinding {
|
|
1813
1893
|
localName: string;
|
|
@@ -1854,7 +1934,7 @@ interface LocalCodeOverlayExcludedFile {
|
|
|
1854
1934
|
}
|
|
1855
1935
|
interface LocalCodeOverlayManifest {
|
|
1856
1936
|
version: "snipara.local_code_overlay.v2";
|
|
1857
|
-
extractorVersion: 2;
|
|
1937
|
+
extractorVersion: 2 | 3;
|
|
1858
1938
|
generatedAt: string;
|
|
1859
1939
|
indexedAt: string;
|
|
1860
1940
|
repoRoot: string;
|
|
@@ -1874,6 +1954,7 @@ interface LocalCodeOverlayManifest {
|
|
|
1874
1954
|
imports: LocalCodeOverlayImport[];
|
|
1875
1955
|
references: LocalCodeOverlayReference[];
|
|
1876
1956
|
edges: LocalCodeOverlayEdge[];
|
|
1957
|
+
semantic?: LocalSemanticModel;
|
|
1877
1958
|
incremental: {
|
|
1878
1959
|
reusedFiles: number;
|
|
1879
1960
|
parsedFiles: number;
|
|
@@ -2010,6 +2091,7 @@ interface LocalCodeOverlaySummary {
|
|
|
2010
2091
|
imports: number;
|
|
2011
2092
|
references: number;
|
|
2012
2093
|
edges: number;
|
|
2094
|
+
semanticAssertions: number;
|
|
2013
2095
|
excluded: number;
|
|
2014
2096
|
};
|
|
2015
2097
|
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,10 @@ 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(
|
|
3826
|
+
{ symbols, edges },
|
|
3827
|
+
{ semanticRules: loadLocalSemanticRuleConfig(repoRoot) }
|
|
3828
|
+
);
|
|
3801
3829
|
for (const file of files) {
|
|
3802
3830
|
file.edgeCount = edges.filter((edge) => edge.filePath === file.path).length;
|
|
3803
3831
|
}
|
|
@@ -3833,7 +3861,7 @@ function buildLocalCodeOverlay(options = {}) {
|
|
|
3833
3861
|
) : null;
|
|
3834
3862
|
return {
|
|
3835
3863
|
version: "snipara.local_code_overlay.v2",
|
|
3836
|
-
extractorVersion:
|
|
3864
|
+
extractorVersion: 3,
|
|
3837
3865
|
generatedAt: now,
|
|
3838
3866
|
indexedAt: now,
|
|
3839
3867
|
repoRoot,
|
|
@@ -3859,6 +3887,7 @@ function buildLocalCodeOverlay(options = {}) {
|
|
|
3859
3887
|
imports,
|
|
3860
3888
|
references: references2,
|
|
3861
3889
|
edges,
|
|
3890
|
+
semantic,
|
|
3862
3891
|
incremental: {
|
|
3863
3892
|
reusedFiles,
|
|
3864
3893
|
parsedFiles,
|
|
@@ -3895,6 +3924,7 @@ function summarizeLocalCodeOverlay(manifest) {
|
|
|
3895
3924
|
imports: manifest.imports.length,
|
|
3896
3925
|
references: manifest.references.length,
|
|
3897
3926
|
edges: manifest.edges.length,
|
|
3927
|
+
semanticAssertions: manifest.semantic?.assertions.length ?? 0,
|
|
3898
3928
|
excluded: manifest.excluded.total
|
|
3899
3929
|
},
|
|
3900
3930
|
incremental: manifest.incremental,
|
|
@@ -4281,6 +4311,541 @@ function uniqueTraversalFiles(nodes, excluded) {
|
|
|
4281
4311
|
)
|
|
4282
4312
|
].sort();
|
|
4283
4313
|
}
|
|
4314
|
+
var LOCAL_SEMANTIC_RULE_CONFIG_PATH = import_path.default.join(".snipara", "semantic-rules.json");
|
|
4315
|
+
var LOCAL_SEMANTIC_RULE_CONFIG_VERSION = "snipara.semantic.rules.v1";
|
|
4316
|
+
var LOCAL_SEMANTIC_MAX_TERMS = 64;
|
|
4317
|
+
var LOCAL_SEMANTIC_MAX_TERM_LENGTH = 80;
|
|
4318
|
+
var LOCAL_SEMANTIC_MAX_ROLES = 24;
|
|
4319
|
+
var LOCAL_SEMANTIC_TEST_PATH = /(^|\/)(tests?|__tests__|fixtures?)(\/|$)|\.(test|spec)\./i;
|
|
4320
|
+
var LOCAL_SEMANTIC_SENSITIVE_PATH = /(^|\/)(auth|middleware|billing|payments?|stripe|webhooks?|prisma|schema|migrations?|deploy)(\/|\.|$)/i;
|
|
4321
|
+
var LOCAL_SEMANTIC_CONTRACT_PATH = /(^|\/)(schemas?|contracts?|protocols?|interfaces?|config|tool_defs)(\/|\.|$)|route\.(ts|tsx|py)$/i;
|
|
4322
|
+
function localSemanticTerms(raw, label, warnings) {
|
|
4323
|
+
if (raw === void 0 || raw === null) return [];
|
|
4324
|
+
if (!Array.isArray(raw)) {
|
|
4325
|
+
warnings.push(`${label} terms must be an array.`);
|
|
4326
|
+
return [];
|
|
4327
|
+
}
|
|
4328
|
+
const terms = [];
|
|
4329
|
+
for (const value of raw.slice(0, LOCAL_SEMANTIC_MAX_TERMS)) {
|
|
4330
|
+
const term = String(value).trim().toLowerCase();
|
|
4331
|
+
if (!term) continue;
|
|
4332
|
+
if (term.length > LOCAL_SEMANTIC_MAX_TERM_LENGTH) {
|
|
4333
|
+
warnings.push(
|
|
4334
|
+
`Ignored a ${label} term longer than ${LOCAL_SEMANTIC_MAX_TERM_LENGTH} characters.`
|
|
4335
|
+
);
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
terms.push(term);
|
|
4339
|
+
}
|
|
4340
|
+
if (raw.length > LOCAL_SEMANTIC_MAX_TERMS) {
|
|
4341
|
+
warnings.push(`Limited ${label} terms to ${LOCAL_SEMANTIC_MAX_TERMS} entries.`);
|
|
4342
|
+
}
|
|
4343
|
+
return [...new Set(terms)];
|
|
4344
|
+
}
|
|
4345
|
+
function loadLocalSemanticRuleConfig(repoRoot) {
|
|
4346
|
+
const defaults = {
|
|
4347
|
+
replaceDefaults: false,
|
|
4348
|
+
sensitivePathTerms: [],
|
|
4349
|
+
contractPathTerms: [],
|
|
4350
|
+
testPathTerms: [],
|
|
4351
|
+
architectureRoleTerms: {},
|
|
4352
|
+
source: "defaults",
|
|
4353
|
+
warnings: []
|
|
4354
|
+
};
|
|
4355
|
+
const configPath = import_path.default.join(repoRoot, LOCAL_SEMANTIC_RULE_CONFIG_PATH);
|
|
4356
|
+
if (!import_fs.default.existsSync(configPath)) return defaults;
|
|
4357
|
+
try {
|
|
4358
|
+
const parsed = JSON.parse(import_fs.default.readFileSync(configPath, "utf8"));
|
|
4359
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4360
|
+
return {
|
|
4361
|
+
...defaults,
|
|
4362
|
+
source: "project-file",
|
|
4363
|
+
warnings: ["Semantic rule config must be a JSON object; defaults were retained."]
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
const raw = parsed;
|
|
4367
|
+
const warnings = [];
|
|
4368
|
+
const rawRoles = raw.architectureRoleTerms ?? raw.architecture_role_terms ?? {};
|
|
4369
|
+
const architectureRoleTerms = {};
|
|
4370
|
+
if (rawRoles && typeof rawRoles === "object" && !Array.isArray(rawRoles)) {
|
|
4371
|
+
for (const [rawRole, rawTerms] of Object.entries(rawRoles).slice(
|
|
4372
|
+
0,
|
|
4373
|
+
LOCAL_SEMANTIC_MAX_ROLES
|
|
4374
|
+
)) {
|
|
4375
|
+
const role = rawRole.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4376
|
+
if (!role) {
|
|
4377
|
+
warnings.push("Ignored an empty or unsupported architecture role name.");
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
architectureRoleTerms[role] = localSemanticTerms(
|
|
4381
|
+
rawTerms,
|
|
4382
|
+
`architecture role ${role}`,
|
|
4383
|
+
warnings
|
|
4384
|
+
);
|
|
4385
|
+
}
|
|
4386
|
+
} else if (rawRoles) {
|
|
4387
|
+
warnings.push("architectureRoleTerms must be an object of role-to-term arrays.");
|
|
4388
|
+
}
|
|
4389
|
+
return {
|
|
4390
|
+
replaceDefaults: Boolean(raw.replaceDefaults ?? raw.replace_defaults ?? false),
|
|
4391
|
+
sensitivePathTerms: localSemanticTerms(
|
|
4392
|
+
raw.sensitivePathTerms ?? raw.sensitive_path_terms,
|
|
4393
|
+
"sensitive path",
|
|
4394
|
+
warnings
|
|
4395
|
+
),
|
|
4396
|
+
contractPathTerms: localSemanticTerms(
|
|
4397
|
+
raw.contractPathTerms ?? raw.contract_path_terms,
|
|
4398
|
+
"contract path",
|
|
4399
|
+
warnings
|
|
4400
|
+
),
|
|
4401
|
+
testPathTerms: localSemanticTerms(
|
|
4402
|
+
raw.testPathTerms ?? raw.test_path_terms,
|
|
4403
|
+
"test path",
|
|
4404
|
+
warnings
|
|
4405
|
+
),
|
|
4406
|
+
architectureRoleTerms,
|
|
4407
|
+
source: "project-file",
|
|
4408
|
+
warnings
|
|
4409
|
+
};
|
|
4410
|
+
} catch (error) {
|
|
4411
|
+
return {
|
|
4412
|
+
...defaults,
|
|
4413
|
+
source: "project-file",
|
|
4414
|
+
warnings: [
|
|
4415
|
+
`Could not parse ${LOCAL_SEMANTIC_RULE_CONFIG_PATH}: ${error instanceof Error ? error.message : "invalid JSON"}`
|
|
4416
|
+
]
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
function localSemanticMatchesTerms(value, terms) {
|
|
4421
|
+
const normalized = value.replace(/\\/g, "/").toLowerCase();
|
|
4422
|
+
return terms.some((term) => normalized.includes(term));
|
|
4423
|
+
}
|
|
4424
|
+
function localSemanticIsTestPath(filePath, rules) {
|
|
4425
|
+
return !rules.replaceDefaults && LOCAL_SEMANTIC_TEST_PATH.test(filePath) || localSemanticMatchesTerms(filePath, rules.testPathTerms);
|
|
4426
|
+
}
|
|
4427
|
+
function localSemanticIsSensitivePath(filePath, rules) {
|
|
4428
|
+
return !rules.replaceDefaults && LOCAL_SEMANTIC_SENSITIVE_PATH.test(filePath) || localSemanticMatchesTerms(filePath, rules.sensitivePathTerms);
|
|
4429
|
+
}
|
|
4430
|
+
function localSemanticIsContractPath(filePath, rules) {
|
|
4431
|
+
return !rules.replaceDefaults && LOCAL_SEMANTIC_CONTRACT_PATH.test(filePath) || localSemanticMatchesTerms(filePath, rules.contractPathTerms);
|
|
4432
|
+
}
|
|
4433
|
+
function localSemanticEvidence(kind, detail, metadata = {}) {
|
|
4434
|
+
return { kind, detail, ...metadata };
|
|
4435
|
+
}
|
|
4436
|
+
function localSemanticAssertion(subject, predicate, value, confidence, source2, evidence) {
|
|
4437
|
+
const identity = JSON.stringify([subject, predicate, value, source2]);
|
|
4438
|
+
return {
|
|
4439
|
+
id: `semantic-${sha2563(identity).slice(0, 16)}`,
|
|
4440
|
+
subject,
|
|
4441
|
+
predicate,
|
|
4442
|
+
value,
|
|
4443
|
+
confidence: Math.round(Math.max(0, Math.min(1, confidence)) * 1e3) / 1e3,
|
|
4444
|
+
scoreKind: "heuristic_prior",
|
|
4445
|
+
calibrated: false,
|
|
4446
|
+
source: source2,
|
|
4447
|
+
extractorVersion: 1,
|
|
4448
|
+
evidence
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
function localArchitectureRoles(symbol, rules) {
|
|
4452
|
+
const haystack = `${symbol.filePath} ${symbol.name}`;
|
|
4453
|
+
const defaultCandidates = [
|
|
4454
|
+
{ role: "repository", pattern: /repository|(^|[_.])repo($|[_.])/i, confidence: 0.9 },
|
|
4455
|
+
{ role: "adapter", pattern: /adapter/i, confidence: 0.9 },
|
|
4456
|
+
{ role: "facade", pattern: /facade/i, confidence: 0.9 },
|
|
4457
|
+
{ role: "controller", pattern: /controller/i, confidence: 0.85 },
|
|
4458
|
+
{ role: "service", pattern: /service/i, confidence: 0.78 },
|
|
4459
|
+
{ role: "worker", pattern: /worker|background[_-]?job|indexer/i, confidence: 0.82 },
|
|
4460
|
+
{ role: "factory", pattern: /factory|builder/i, confidence: 0.76 }
|
|
4461
|
+
];
|
|
4462
|
+
const roles = [];
|
|
4463
|
+
if (/\/app\/api\//i.test(symbol.filePath) || /\/route\.(ts|tsx|py)$/i.test(symbol.filePath)) {
|
|
4464
|
+
roles.push(
|
|
4465
|
+
localSemanticAssertion(
|
|
4466
|
+
symbol.localKey,
|
|
4467
|
+
"architecture_role",
|
|
4468
|
+
"route_handler",
|
|
4469
|
+
0.94,
|
|
4470
|
+
"architecture-pattern-rules",
|
|
4471
|
+
[
|
|
4472
|
+
localSemanticEvidence("route_path", "Route path identifies an API boundary.", {
|
|
4473
|
+
filePath: symbol.filePath,
|
|
4474
|
+
line: symbol.line
|
|
4475
|
+
})
|
|
4476
|
+
]
|
|
4477
|
+
)
|
|
4478
|
+
);
|
|
4479
|
+
}
|
|
4480
|
+
if (localSemanticIsTestPath(symbol.filePath, rules)) {
|
|
4481
|
+
roles.push(
|
|
4482
|
+
localSemanticAssertion(
|
|
4483
|
+
symbol.localKey,
|
|
4484
|
+
"architecture_role",
|
|
4485
|
+
"test",
|
|
4486
|
+
0.98,
|
|
4487
|
+
"architecture-pattern-rules",
|
|
4488
|
+
[localSemanticEvidence("test_path", "File is under a test surface.")]
|
|
4489
|
+
)
|
|
4490
|
+
);
|
|
4491
|
+
}
|
|
4492
|
+
for (const candidate of rules.replaceDefaults ? [] : defaultCandidates) {
|
|
4493
|
+
if (!candidate.pattern.test(haystack)) continue;
|
|
4494
|
+
roles.push(
|
|
4495
|
+
localSemanticAssertion(
|
|
4496
|
+
symbol.localKey,
|
|
4497
|
+
"architecture_role",
|
|
4498
|
+
candidate.role,
|
|
4499
|
+
candidate.confidence,
|
|
4500
|
+
"architecture-pattern-rules",
|
|
4501
|
+
[
|
|
4502
|
+
localSemanticEvidence(
|
|
4503
|
+
"name_or_path_pattern",
|
|
4504
|
+
`Name or path matches the ${candidate.role} pattern.`,
|
|
4505
|
+
{ filePath: symbol.filePath, line: symbol.line }
|
|
4506
|
+
)
|
|
4507
|
+
]
|
|
4508
|
+
)
|
|
4509
|
+
);
|
|
4510
|
+
}
|
|
4511
|
+
const existingRoles = new Set(roles.map((assertion) => String(assertion.value)));
|
|
4512
|
+
for (const [role, terms] of Object.entries(rules.architectureRoleTerms)) {
|
|
4513
|
+
if (existingRoles.has(role) || !localSemanticMatchesTerms(haystack, terms)) continue;
|
|
4514
|
+
roles.push(
|
|
4515
|
+
localSemanticAssertion(
|
|
4516
|
+
symbol.localKey,
|
|
4517
|
+
"architecture_role",
|
|
4518
|
+
role,
|
|
4519
|
+
0.7,
|
|
4520
|
+
"architecture-pattern-rules",
|
|
4521
|
+
[
|
|
4522
|
+
localSemanticEvidence(
|
|
4523
|
+
"project_role_term",
|
|
4524
|
+
`Name or path matches a configured ${role} project term.`,
|
|
4525
|
+
{ filePath: symbol.filePath, line: symbol.line }
|
|
4526
|
+
)
|
|
4527
|
+
]
|
|
4528
|
+
)
|
|
4529
|
+
);
|
|
4530
|
+
}
|
|
4531
|
+
return roles;
|
|
4532
|
+
}
|
|
4533
|
+
function localSymbolSemanticAssertions(symbol, rules) {
|
|
4534
|
+
const assertions = [];
|
|
4535
|
+
const routeSurface = /\/app\/api\//i.test(symbol.filePath) || /\/route\.(ts|tsx|py)$/i.test(symbol.filePath);
|
|
4536
|
+
const toolSurface = /(^|\/)(mcp|tool_defs)(\/|\.|$)/i.test(symbol.filePath);
|
|
4537
|
+
const publicEvidence = [];
|
|
4538
|
+
if (symbol.exported) {
|
|
4539
|
+
publicEvidence.push(
|
|
4540
|
+
localSemanticEvidence(
|
|
4541
|
+
"explicit_export",
|
|
4542
|
+
"TypeScript/Go declaration is explicitly exported.",
|
|
4543
|
+
{
|
|
4544
|
+
filePath: symbol.filePath,
|
|
4545
|
+
line: symbol.line
|
|
4546
|
+
}
|
|
4547
|
+
)
|
|
4548
|
+
);
|
|
4549
|
+
}
|
|
4550
|
+
if (routeSurface) {
|
|
4551
|
+
publicEvidence.push(
|
|
4552
|
+
localSemanticEvidence("route_path", "Symbol is declared in a route module.", {
|
|
4553
|
+
filePath: symbol.filePath,
|
|
4554
|
+
line: symbol.line
|
|
4555
|
+
})
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
if (toolSurface) {
|
|
4559
|
+
publicEvidence.push(
|
|
4560
|
+
localSemanticEvidence(
|
|
4561
|
+
"tool_surface",
|
|
4562
|
+
"Symbol is declared in an MCP/tool-definition surface.",
|
|
4563
|
+
{
|
|
4564
|
+
filePath: symbol.filePath,
|
|
4565
|
+
line: symbol.line
|
|
4566
|
+
}
|
|
4567
|
+
)
|
|
4568
|
+
);
|
|
4569
|
+
}
|
|
4570
|
+
if (publicEvidence.length > 0) {
|
|
4571
|
+
assertions.push(
|
|
4572
|
+
localSemanticAssertion(
|
|
4573
|
+
symbol.localKey,
|
|
4574
|
+
"public_api",
|
|
4575
|
+
true,
|
|
4576
|
+
Math.min(0.98, 0.82 + publicEvidence.length * 0.05),
|
|
4577
|
+
symbol.exported ? "typescript-compiler-api" : "architecture-pattern-rules",
|
|
4578
|
+
publicEvidence
|
|
4579
|
+
)
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4582
|
+
let contractKind = null;
|
|
4583
|
+
const contractEvidence = [];
|
|
4584
|
+
if (symbol.exported && ["interface", "type"].includes(symbol.kind)) {
|
|
4585
|
+
contractKind = "exported_type_contract";
|
|
4586
|
+
contractEvidence.push(
|
|
4587
|
+
localSemanticEvidence("symbol_kind", `Exported ${symbol.kind} defines a structural contract.`)
|
|
4588
|
+
);
|
|
4589
|
+
} else if (localSemanticIsContractPath(symbol.filePath, rules)) {
|
|
4590
|
+
contractKind = "repository_boundary_contract";
|
|
4591
|
+
contractEvidence.push(
|
|
4592
|
+
localSemanticEvidence(
|
|
4593
|
+
"contract_path",
|
|
4594
|
+
"File path denotes a schema, contract, config, route, or tool boundary."
|
|
4595
|
+
)
|
|
4596
|
+
);
|
|
4597
|
+
} else if (symbol.exported) {
|
|
4598
|
+
contractKind = "public_surface_contract";
|
|
4599
|
+
contractEvidence.push(...publicEvidence.slice(0, 1));
|
|
4600
|
+
}
|
|
4601
|
+
if (contractKind) {
|
|
4602
|
+
assertions.push(
|
|
4603
|
+
localSemanticAssertion(
|
|
4604
|
+
symbol.localKey,
|
|
4605
|
+
"implicit_contract",
|
|
4606
|
+
contractKind,
|
|
4607
|
+
["interface", "type"].includes(symbol.kind) ? 0.92 : 0.78,
|
|
4608
|
+
symbol.exported ? "typescript-compiler-api" : "architecture-pattern-rules",
|
|
4609
|
+
contractEvidence
|
|
4610
|
+
)
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
assertions.push(...localArchitectureRoles(symbol, rules));
|
|
4614
|
+
let criticality = "ordinary";
|
|
4615
|
+
let criticalityConfidence = 0.58;
|
|
4616
|
+
let criticalityEvidence = [
|
|
4617
|
+
localSemanticEvidence("default", "No critical or incidental evidence matched.")
|
|
4618
|
+
];
|
|
4619
|
+
if (localSemanticIsTestPath(symbol.filePath, rules)) {
|
|
4620
|
+
criticality = "incidental";
|
|
4621
|
+
criticalityConfidence = 0.95;
|
|
4622
|
+
criticalityEvidence = [localSemanticEvidence("test_path", "Test-only dependency.")];
|
|
4623
|
+
} else if (localSemanticIsSensitivePath(symbol.filePath, rules) || routeSurface || toolSurface) {
|
|
4624
|
+
criticality = "critical";
|
|
4625
|
+
criticalityConfidence = 0.92;
|
|
4626
|
+
criticalityEvidence = [
|
|
4627
|
+
localSemanticEvidence(
|
|
4628
|
+
"sensitive_surface",
|
|
4629
|
+
"Path identifies an auth, billing, schema, webhook, deploy, route, or MCP surface."
|
|
4630
|
+
)
|
|
4631
|
+
];
|
|
4632
|
+
} else if (publicEvidence.length > 0) {
|
|
4633
|
+
criticality = "important";
|
|
4634
|
+
criticalityConfidence = 0.85;
|
|
4635
|
+
criticalityEvidence = [
|
|
4636
|
+
localSemanticEvidence(
|
|
4637
|
+
"exported_surface",
|
|
4638
|
+
"Explicit export defines a module contract but is not by itself a critical runtime surface."
|
|
4639
|
+
)
|
|
4640
|
+
];
|
|
4641
|
+
} else if (contractKind) {
|
|
4642
|
+
criticality = "important";
|
|
4643
|
+
criticalityConfidence = 0.82;
|
|
4644
|
+
criticalityEvidence = [
|
|
4645
|
+
localSemanticEvidence("contract_surface", "Dependency defines a repository contract.")
|
|
4646
|
+
];
|
|
4647
|
+
}
|
|
4648
|
+
assertions.push(
|
|
4649
|
+
localSemanticAssertion(
|
|
4650
|
+
symbol.localKey,
|
|
4651
|
+
"dependency_criticality",
|
|
4652
|
+
criticality,
|
|
4653
|
+
criticalityConfidence,
|
|
4654
|
+
"criticality-rules",
|
|
4655
|
+
criticalityEvidence
|
|
4656
|
+
)
|
|
4657
|
+
);
|
|
4658
|
+
return assertions;
|
|
4659
|
+
}
|
|
4660
|
+
function buildLocalSemanticModel(manifest, options = {}) {
|
|
4661
|
+
const rules = options.semanticRules ?? loadLocalSemanticRuleConfig(process.cwd());
|
|
4662
|
+
const symbols = options.symbolKeys ? manifest.symbols.filter((symbol) => options.symbolKeys?.has(symbol.localKey)) : manifest.symbols;
|
|
4663
|
+
const assertions = symbols.flatMap((symbol) => localSymbolSemanticAssertions(symbol, rules));
|
|
4664
|
+
const symbolByKey = new Map(manifest.symbols.map((symbol) => [symbol.localKey, symbol]));
|
|
4665
|
+
const criticalityBySymbol = new Map(
|
|
4666
|
+
assertions.filter((assertion) => assertion.predicate === "dependency_criticality").map((assertion) => [assertion.subject, String(assertion.value)])
|
|
4667
|
+
);
|
|
4668
|
+
const criticalityRank = {
|
|
4669
|
+
incidental: 0,
|
|
4670
|
+
ordinary: 1,
|
|
4671
|
+
important: 2,
|
|
4672
|
+
critical: 3
|
|
4673
|
+
};
|
|
4674
|
+
const criticalityByFile = /* @__PURE__ */ new Map();
|
|
4675
|
+
for (const symbol of manifest.symbols) {
|
|
4676
|
+
const value = criticalityBySymbol.get(symbol.localKey);
|
|
4677
|
+
if (!value) continue;
|
|
4678
|
+
const current = criticalityByFile.get(symbol.filePath);
|
|
4679
|
+
if (!current || criticalityRank[value] > criticalityRank[current]) {
|
|
4680
|
+
criticalityByFile.set(symbol.filePath, value);
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
for (const edge of options.edges ?? []) {
|
|
4684
|
+
const fromSymbol = symbolByKey.get(edge.from);
|
|
4685
|
+
const toSymbol = symbolByKey.get(edge.to);
|
|
4686
|
+
const fromFile = fromSymbol?.filePath ?? edge.from.replace(/^local-file::/, "");
|
|
4687
|
+
const toFile = toSymbol?.filePath ?? edge.to.replace(/^local-file::/, "");
|
|
4688
|
+
const fromCriticality = criticalityBySymbol.get(edge.from) ?? criticalityByFile.get(fromFile);
|
|
4689
|
+
const toCriticality = criticalityBySymbol.get(edge.to) ?? criticalityByFile.get(toFile);
|
|
4690
|
+
const edgeSubject = `edge::${edge.from}::${edge.kind}::${edge.to}`;
|
|
4691
|
+
const evidence = [
|
|
4692
|
+
localSemanticEvidence("graph_edge", `${edge.kind} from ${fromFile} to ${toFile}.`, {
|
|
4693
|
+
filePath: edge.filePath,
|
|
4694
|
+
line: edge.line,
|
|
4695
|
+
edgeKind: edge.kind
|
|
4696
|
+
})
|
|
4697
|
+
];
|
|
4698
|
+
let value = "ordinary";
|
|
4699
|
+
let confidence = 0.62;
|
|
4700
|
+
if (localSemanticIsTestPath(fromFile, rules) || localSemanticIsTestPath(toFile, rules)) {
|
|
4701
|
+
value = "incidental";
|
|
4702
|
+
confidence = 0.92;
|
|
4703
|
+
evidence.push(localSemanticEvidence("test_path", "At least one edge endpoint is test-only."));
|
|
4704
|
+
} else if (fromCriticality === "critical" || toCriticality === "critical") {
|
|
4705
|
+
value = "critical";
|
|
4706
|
+
confidence = 0.9;
|
|
4707
|
+
evidence.push(
|
|
4708
|
+
localSemanticEvidence("critical_endpoint", "At least one dependency endpoint is critical.")
|
|
4709
|
+
);
|
|
4710
|
+
} else if (edge.kind === "CONTAINS" || edge.confidence < 0.65) {
|
|
4711
|
+
value = "incidental";
|
|
4712
|
+
confidence = 0.78;
|
|
4713
|
+
evidence.push(
|
|
4714
|
+
localSemanticEvidence(
|
|
4715
|
+
"weak_or_structural",
|
|
4716
|
+
"Containment or low-confidence evidence is incidental to change propagation."
|
|
4717
|
+
)
|
|
4718
|
+
);
|
|
4719
|
+
} else if (fromCriticality === "important" || toCriticality === "important") {
|
|
4720
|
+
value = "important";
|
|
4721
|
+
confidence = 0.82;
|
|
4722
|
+
evidence.push(
|
|
4723
|
+
localSemanticEvidence(
|
|
4724
|
+
"contract_endpoint",
|
|
4725
|
+
"At least one dependency endpoint is a contract."
|
|
4726
|
+
)
|
|
4727
|
+
);
|
|
4728
|
+
}
|
|
4729
|
+
assertions.push(
|
|
4730
|
+
localSemanticAssertion(
|
|
4731
|
+
edgeSubject,
|
|
4732
|
+
"dependency_criticality",
|
|
4733
|
+
value,
|
|
4734
|
+
confidence,
|
|
4735
|
+
"graph-criticality-rules",
|
|
4736
|
+
evidence
|
|
4737
|
+
)
|
|
4738
|
+
);
|
|
4739
|
+
}
|
|
4740
|
+
const allAssertions = [
|
|
4741
|
+
...new Map(assertions.map((assertion) => [assertion.id, assertion])).values()
|
|
4742
|
+
].sort(
|
|
4743
|
+
(left, right) => `${left.subject}:${left.predicate}:${String(left.value)}`.localeCompare(
|
|
4744
|
+
`${right.subject}:${right.predicate}:${String(right.value)}`
|
|
4745
|
+
)
|
|
4746
|
+
);
|
|
4747
|
+
const assertionLimit = (options.scope ?? "repository") === "impact" ? 2e3 : 5e3;
|
|
4748
|
+
const returnedAssertions = allAssertions.slice(0, assertionLimit);
|
|
4749
|
+
const allPublicContracts = allAssertions.filter(
|
|
4750
|
+
(assertion) => assertion.predicate === "public_api" || assertion.predicate === "implicit_contract"
|
|
4751
|
+
);
|
|
4752
|
+
const allArchitectureRoles = allAssertions.filter(
|
|
4753
|
+
(assertion) => assertion.predicate === "architecture_role"
|
|
4754
|
+
);
|
|
4755
|
+
const allDependencyCriticality = allAssertions.filter(
|
|
4756
|
+
(assertion) => assertion.predicate === "dependency_criticality"
|
|
4757
|
+
);
|
|
4758
|
+
const publicContracts = returnedAssertions.filter(
|
|
4759
|
+
(assertion) => assertion.predicate === "public_api" || assertion.predicate === "implicit_contract"
|
|
4760
|
+
);
|
|
4761
|
+
const architectureRoles = returnedAssertions.filter(
|
|
4762
|
+
(assertion) => assertion.predicate === "architecture_role"
|
|
4763
|
+
);
|
|
4764
|
+
const dependencyCriticality = returnedAssertions.filter(
|
|
4765
|
+
(assertion) => assertion.predicate === "dependency_criticality"
|
|
4766
|
+
);
|
|
4767
|
+
return {
|
|
4768
|
+
version: "snipara.semantic.v1",
|
|
4769
|
+
extractorVersion: 1,
|
|
4770
|
+
source: "deterministic-graph-inference",
|
|
4771
|
+
modelType: "rule-based-heuristic",
|
|
4772
|
+
scoreContract: {
|
|
4773
|
+
kind: "heuristic_prior",
|
|
4774
|
+
calibrated: false,
|
|
4775
|
+
probability: false,
|
|
4776
|
+
comparableAcrossProjects: false,
|
|
4777
|
+
basis: "hand-tuned-v1",
|
|
4778
|
+
rulesetVersion: 1,
|
|
4779
|
+
interpretation: "Relative rule strength for ranking and explanation; not an observed probability."
|
|
4780
|
+
},
|
|
4781
|
+
ruleConfig: {
|
|
4782
|
+
version: LOCAL_SEMANTIC_RULE_CONFIG_VERSION,
|
|
4783
|
+
source: rules.source,
|
|
4784
|
+
path: ".snipara/semantic-rules.json",
|
|
4785
|
+
replaceDefaults: rules.replaceDefaults,
|
|
4786
|
+
sensitivePathTermCount: rules.sensitivePathTerms.length,
|
|
4787
|
+
contractPathTermCount: rules.contractPathTerms.length,
|
|
4788
|
+
testPathTermCount: rules.testPathTerms.length,
|
|
4789
|
+
architectureRoleTermCount: Object.values(rules.architectureRoleTerms).reduce(
|
|
4790
|
+
(total, terms) => total + terms.length,
|
|
4791
|
+
0
|
|
4792
|
+
),
|
|
4793
|
+
configuredRoles: Object.keys(rules.architectureRoleTerms).sort(),
|
|
4794
|
+
configHash: sha2563(
|
|
4795
|
+
JSON.stringify({
|
|
4796
|
+
replaceDefaults: rules.replaceDefaults,
|
|
4797
|
+
sensitivePathTerms: rules.sensitivePathTerms,
|
|
4798
|
+
contractPathTerms: rules.contractPathTerms,
|
|
4799
|
+
testPathTerms: rules.testPathTerms,
|
|
4800
|
+
architectureRoleTerms: rules.architectureRoleTerms
|
|
4801
|
+
})
|
|
4802
|
+
).slice(0, 16),
|
|
4803
|
+
warnings: rules.warnings
|
|
4804
|
+
},
|
|
4805
|
+
scope: options.scope ?? "repository",
|
|
4806
|
+
assertions: returnedAssertions,
|
|
4807
|
+
publicContracts,
|
|
4808
|
+
architectureRoles,
|
|
4809
|
+
dependencyCriticality,
|
|
4810
|
+
truncation: {
|
|
4811
|
+
truncated: allAssertions.length > assertionLimit,
|
|
4812
|
+
totalAssertionCount: allAssertions.length,
|
|
4813
|
+
returnedAssertionCount: returnedAssertions.length,
|
|
4814
|
+
assertionLimit
|
|
4815
|
+
},
|
|
4816
|
+
historicalRegression: {
|
|
4817
|
+
mode: "shadow",
|
|
4818
|
+
sampleThreshold: 3,
|
|
4819
|
+
failureEventCount: 0,
|
|
4820
|
+
associations: [],
|
|
4821
|
+
riskContributionEnabled: false,
|
|
4822
|
+
suggestedRiskDelta: 0,
|
|
4823
|
+
caveat: "The local overlay has no durable regression outcome stream. Historical path learning stays shadow-only until hosted evidence is available."
|
|
4824
|
+
},
|
|
4825
|
+
summary: {
|
|
4826
|
+
assertionCount: allAssertions.length,
|
|
4827
|
+
publicContractCount: allPublicContracts.length,
|
|
4828
|
+
architectureRoleCount: allArchitectureRoles.length,
|
|
4829
|
+
criticalDependencyCount: allDependencyCriticality.filter(
|
|
4830
|
+
(assertion) => assertion.value === "critical"
|
|
4831
|
+
).length,
|
|
4832
|
+
criticalEdgeCount: allDependencyCriticality.filter(
|
|
4833
|
+
(assertion) => assertion.value === "critical" && assertion.subject.startsWith("edge::")
|
|
4834
|
+
).length,
|
|
4835
|
+
incidentalDependencyCount: allDependencyCriticality.filter(
|
|
4836
|
+
(assertion) => assertion.value === "incidental"
|
|
4837
|
+
).length,
|
|
4838
|
+
historicalAssociationCount: 0
|
|
4839
|
+
},
|
|
4840
|
+
caveats: [
|
|
4841
|
+
"Semantic assertions are deterministic inferences, not declarations of author intent.",
|
|
4842
|
+
"Confidence is an uncalibrated rule-based prior, not a probability or measured accuracy.",
|
|
4843
|
+
"Priors are not comparable across projects with different semantic rule configuration.",
|
|
4844
|
+
"Name-only architecture patterns use a lower prior than explicit export or path evidence.",
|
|
4845
|
+
"Historical regression learning requires hosted outcome evidence and remains shadow-only."
|
|
4846
|
+
]
|
|
4847
|
+
};
|
|
4848
|
+
}
|
|
4284
4849
|
function loadLocalRiskConfig(repoRoot) {
|
|
4285
4850
|
const defaults = {
|
|
4286
4851
|
depthDecay: 0.65,
|
|
@@ -4305,7 +4870,7 @@ function loadLocalRiskConfig(repoRoot) {
|
|
|
4305
4870
|
return defaults;
|
|
4306
4871
|
}
|
|
4307
4872
|
}
|
|
4308
|
-
function buildLocalRisk(manifest, changedFiles, traversal, depth) {
|
|
4873
|
+
function buildLocalRisk(manifest, changedFiles, traversal, depth, semantic) {
|
|
4309
4874
|
const config = loadLocalRiskConfig(manifest.repoRoot);
|
|
4310
4875
|
const allFiles = [
|
|
4311
4876
|
...changedFiles,
|
|
@@ -4344,13 +4909,28 @@ function buildLocalRisk(manifest, changedFiles, traversal, depth) {
|
|
|
4344
4909
|
(reference) => changedFiles.includes(reference.filePath) && !resolvedReferenceKeys.has(`${reference.from}:${reference.line}`)
|
|
4345
4910
|
).length;
|
|
4346
4911
|
const uncertaintyPenalty = Math.min(15, unresolvedReferences * 2 + (traversal.truncated ? 8 : 0));
|
|
4347
|
-
const
|
|
4912
|
+
const criticalSemanticDependencies = semantic.summary.criticalEdgeCount;
|
|
4913
|
+
const changedSymbolKeys = new Set(
|
|
4914
|
+
manifest.symbols.filter((symbol) => changedFiles.includes(symbol.filePath)).map((symbol) => symbol.localKey)
|
|
4915
|
+
);
|
|
4916
|
+
const semanticRules = loadLocalSemanticRuleConfig(manifest.repoRoot);
|
|
4917
|
+
const changedContracts = manifest.symbols.filter((symbol) => changedSymbolKeys.has(symbol.localKey)).flatMap((symbol) => localSymbolSemanticAssertions(symbol, semanticRules)).filter(
|
|
4918
|
+
(assertion) => assertion.predicate === "public_api" || assertion.predicate === "implicit_contract"
|
|
4919
|
+
).length;
|
|
4920
|
+
const semanticRiskPoints = Math.min(20, criticalSemanticDependencies * 3) + Math.min(10, changedContracts * 2);
|
|
4921
|
+
const rawScore = criticality * (directFiles.size * 8 + propagation * 4) + uncertaintyPenalty + semanticRiskPoints;
|
|
4348
4922
|
const score = Math.max(0, Math.min(100, Math.round(rawScore)));
|
|
4349
4923
|
const reasons = [
|
|
4350
4924
|
`${directFiles.size} direct dependent file${directFiles.size === 1 ? "" : "s"}`,
|
|
4351
4925
|
`${transitiveFiles.size} transitive dependent file${transitiveFiles.size === 1 ? "" : "s"}`,
|
|
4352
4926
|
...criticalMatches.length ? [`critical-surface multiplier ${criticality.toFixed(2)}`] : [],
|
|
4353
4927
|
...unresolvedReferences ? [`${unresolvedReferences} unresolved reference${unresolvedReferences === 1 ? "" : "s"}`] : [],
|
|
4928
|
+
...criticalSemanticDependencies ? [
|
|
4929
|
+
`${criticalSemanticDependencies} critical semantic dependenc${criticalSemanticDependencies === 1 ? "y" : "ies"}`
|
|
4930
|
+
] : [],
|
|
4931
|
+
...changedContracts ? [
|
|
4932
|
+
`${changedContracts} public or implicit contract assertion${changedContracts === 1 ? "" : "s"}`
|
|
4933
|
+
] : [],
|
|
4354
4934
|
...traversal.truncated ? ["traversal truncated by max-nodes"] : []
|
|
4355
4935
|
];
|
|
4356
4936
|
return {
|
|
@@ -4361,9 +4941,12 @@ function buildLocalRisk(manifest, changedFiles, traversal, depth) {
|
|
|
4361
4941
|
transitiveDependentFiles: transitiveFiles.size,
|
|
4362
4942
|
criticality,
|
|
4363
4943
|
uncertaintyPenalty,
|
|
4944
|
+
semanticRiskPoints,
|
|
4945
|
+
criticalSemanticDependencies,
|
|
4946
|
+
changedContracts,
|
|
4364
4947
|
unresolvedReferences,
|
|
4365
4948
|
reasons,
|
|
4366
|
-
formula: "criticality * (directImpact + decayedEdgePropagation) + uncertaintyPenalty"
|
|
4949
|
+
formula: "criticality * (directImpact + decayedEdgePropagation) + uncertaintyPenalty + semanticRiskPoints"
|
|
4367
4950
|
};
|
|
4368
4951
|
}
|
|
4369
4952
|
function missingTargetDetail(manifest, filePath) {
|
|
@@ -4679,6 +5262,42 @@ function hybridRisk(local, hosted) {
|
|
|
4679
5262
|
)
|
|
4680
5263
|
};
|
|
4681
5264
|
}
|
|
5265
|
+
function hybridSemantic(local, hosted) {
|
|
5266
|
+
const localSemantic = asUnknownRecord(local.semantic);
|
|
5267
|
+
const hostedSemantic = asUnknownRecord(hosted.semantic);
|
|
5268
|
+
if (Object.keys(localSemantic).length === 0 && Object.keys(hostedSemantic).length === 0) {
|
|
5269
|
+
return void 0;
|
|
5270
|
+
}
|
|
5271
|
+
const assertions = mergeResultArrays(
|
|
5272
|
+
[{ assertions: hostedSemantic.assertions }, { assertions: localSemantic.assertions }],
|
|
5273
|
+
["assertions"]
|
|
5274
|
+
);
|
|
5275
|
+
return {
|
|
5276
|
+
version: "snipara.semantic.hybrid.v1",
|
|
5277
|
+
mode: "hybrid",
|
|
5278
|
+
provenance: {
|
|
5279
|
+
canonicalBase: Object.keys(hostedSemantic).length > 0 ? "hosted_graph" : "unavailable",
|
|
5280
|
+
checkoutDelta: Object.keys(localSemantic).length > 0 ? "local_overlay" : "unavailable",
|
|
5281
|
+
crossSourceAssertionSynthesis: false
|
|
5282
|
+
},
|
|
5283
|
+
assertions,
|
|
5284
|
+
summary: {
|
|
5285
|
+
assertionCount: assertions.length,
|
|
5286
|
+
publicContractCount: assertions.filter(
|
|
5287
|
+
(item) => asUnknownRecord(item).predicate === "public_api" || asUnknownRecord(item).predicate === "implicit_contract"
|
|
5288
|
+
).length,
|
|
5289
|
+
architectureRoleCount: assertions.filter(
|
|
5290
|
+
(item) => asUnknownRecord(item).predicate === "architecture_role"
|
|
5291
|
+
).length,
|
|
5292
|
+
criticalDependencyCount: assertions.filter(
|
|
5293
|
+
(item) => asUnknownRecord(item).predicate === "dependency_criticality" && asUnknownRecord(item).value === "critical"
|
|
5294
|
+
).length
|
|
5295
|
+
},
|
|
5296
|
+
historicalRegression: hostedSemantic.historical_regression ?? hostedSemantic.historicalRegression ?? localSemantic.historicalRegression ?? null,
|
|
5297
|
+
local: Object.keys(localSemantic).length > 0 ? localSemantic : null,
|
|
5298
|
+
hosted: Object.keys(hostedSemantic).length > 0 ? hostedSemantic : null
|
|
5299
|
+
};
|
|
5300
|
+
}
|
|
4682
5301
|
function mergeHybridCodeResults(verb, localResult, hostedResult, hostedError) {
|
|
4683
5302
|
const local = asUnknownRecord(localResult);
|
|
4684
5303
|
const hosted = asUnknownRecord(hostedResult);
|
|
@@ -4698,6 +5317,7 @@ function mergeHybridCodeResults(verb, localResult, hostedResult, hostedError) {
|
|
|
4698
5317
|
collectStringValues(local, /* @__PURE__ */ new Set(["changed_files", "changedFiles"]), changedFiles);
|
|
4699
5318
|
collectStringValues(hosted, /* @__PURE__ */ new Set(["changed_files", "changedFiles"]), changedFiles);
|
|
4700
5319
|
const risk = hybridRisk(local, hosted);
|
|
5320
|
+
const semantic = hybridSemantic(local, hosted);
|
|
4701
5321
|
const recommendedTests = mergeResultArrays(
|
|
4702
5322
|
[hosted, local],
|
|
4703
5323
|
["recommended_tests", "recommendedTests", "related_tests", "relatedTests", "tests"]
|
|
@@ -4725,6 +5345,7 @@ function mergeHybridCodeResults(verb, localResult, hostedResult, hostedError) {
|
|
|
4725
5345
|
changed_files: [...changedFiles].sort(),
|
|
4726
5346
|
impactedFiles: [...affectedFiles].sort(),
|
|
4727
5347
|
...risk ? { risk } : {},
|
|
5348
|
+
...semantic ? { semantic } : {},
|
|
4728
5349
|
recommended_tests: recommendedTests,
|
|
4729
5350
|
recommended_actions: recommendedActions,
|
|
4730
5351
|
coverage_gaps: coverageGaps,
|
|
@@ -5342,6 +5963,25 @@ function buildLocalImpactResult(options) {
|
|
|
5342
5963
|
edgeKinds: options.edgeKinds?.length ? options.edgeKinds : ["IMPORTS", "CALLS", "REFERENCES"],
|
|
5343
5964
|
maxNodes
|
|
5344
5965
|
});
|
|
5966
|
+
const traversalEdges = [
|
|
5967
|
+
...new Map(
|
|
5968
|
+
traversal.nodes.flatMap((node) => node.edges).map((edge) => [`${edge.from}:${edge.kind}:${edge.to}:${edge.line}`, edge])
|
|
5969
|
+
).values()
|
|
5970
|
+
];
|
|
5971
|
+
const semanticSymbolKeys = /* @__PURE__ */ new Set([...seedKeys, ...traversal.nodes.map((node) => node.key)]);
|
|
5972
|
+
const semanticFiles = /* @__PURE__ */ new Set([
|
|
5973
|
+
...changedFiles,
|
|
5974
|
+
...traversal.nodes.flatMap((node) => node.filePath ? [node.filePath] : [])
|
|
5975
|
+
]);
|
|
5976
|
+
for (const candidate of manifest.symbols) {
|
|
5977
|
+
if (semanticFiles.has(candidate.filePath)) semanticSymbolKeys.add(candidate.localKey);
|
|
5978
|
+
}
|
|
5979
|
+
const semantic = buildLocalSemanticModel(manifest, {
|
|
5980
|
+
scope: "impact",
|
|
5981
|
+
symbolKeys: semanticSymbolKeys,
|
|
5982
|
+
edges: traversalEdges,
|
|
5983
|
+
semanticRules: loadLocalSemanticRuleConfig(manifest.repoRoot)
|
|
5984
|
+
});
|
|
5345
5985
|
const transitiveFiles = uniqueTraversalFiles(traversal.nodes, selectedFiles);
|
|
5346
5986
|
const impactedFiles = [.../* @__PURE__ */ new Set([...directImpactedFiles, ...transitiveFiles])].sort();
|
|
5347
5987
|
const relevantWarningFiles = /* @__PURE__ */ new Set([
|
|
@@ -5382,7 +6022,8 @@ function buildLocalImpactResult(options) {
|
|
|
5382
6022
|
visitedCount: traversal.visitedCount,
|
|
5383
6023
|
maxNodes
|
|
5384
6024
|
},
|
|
5385
|
-
|
|
6025
|
+
semantic,
|
|
6026
|
+
risk: buildLocalRisk(manifest, changedFiles, traversal, depth, semantic)
|
|
5386
6027
|
};
|
|
5387
6028
|
}
|
|
5388
6029
|
async function codeLocalImportsCommand(options) {
|
|
@@ -0,0 +1,106 @@
|
|
|
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 backward-compatible bounded rule-strength prior from `0` to
|
|
18
|
+
`1`, explicitly qualified by `scoreKind: "heuristic_prior"` and
|
|
19
|
+
`calibrated: false`;
|
|
20
|
+
- `source` and `extractorVersion`;
|
|
21
|
+
- `evidence`: the export modifier, symbol kind, file/path pattern, or graph edge
|
|
22
|
+
that produced the assertion.
|
|
23
|
+
|
|
24
|
+
The extractor recognizes explicit TypeScript and Go exports, route and MCP
|
|
25
|
+
surfaces, exported type contracts, and common Repository, Adapter, Facade,
|
|
26
|
+
Controller, Service, Worker, Factory, schema, route-handler, and test roles.
|
|
27
|
+
`confidence` is not a probability, measured accuracy, or cross-project metric.
|
|
28
|
+
It is a deterministic prior used for relative ranking and explanation inside
|
|
29
|
+
one rule configuration. `semantic.scoreContract.basis` is
|
|
30
|
+
`hand-tuned-v1`; the values must be calibrated against measured outcomes before
|
|
31
|
+
that status can change. Name-only patterns have a lower prior than explicit
|
|
32
|
+
export, route, or graph evidence.
|
|
33
|
+
|
|
34
|
+
### Project naming rules
|
|
35
|
+
|
|
36
|
+
Projects can extend or replace built-in English naming conventions with
|
|
37
|
+
`.snipara/semantic-rules.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"replaceDefaults": false,
|
|
42
|
+
"sensitivePathTerms": ["securite", "facturation"],
|
|
43
|
+
"contractPathTerms": ["contrats"],
|
|
44
|
+
"testPathTerms": ["essais"],
|
|
45
|
+
"architectureRoleTerms": {
|
|
46
|
+
"repository": ["depot"],
|
|
47
|
+
"service": ["metier"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Terms are case-insensitive literal strings, not regular expressions. Each group
|
|
53
|
+
is limited to 64 terms of at most 80 characters and role names are normalized.
|
|
54
|
+
Invalid config is reported in `semantic.ruleConfig.warnings`; it never aborts
|
|
55
|
+
overlay construction. Set `replaceDefaults: true` only when the project wants
|
|
56
|
+
configured naming terms to replace built-in naming conventions. Explicit
|
|
57
|
+
exports, route surfaces, and graph evidence continue to apply.
|
|
58
|
+
|
|
59
|
+
## Dependency criticality
|
|
60
|
+
|
|
61
|
+
Criticality is evaluated for symbols and for the edges traversed by impact:
|
|
62
|
+
|
|
63
|
+
- `critical`: runtime-exposed route/MCP surfaces and sensitive auth, billing,
|
|
64
|
+
schema, webhook, or deploy paths;
|
|
65
|
+
- `important`: explicit module exports and inferred contract boundaries;
|
|
66
|
+
- `ordinary`: normal application dependencies without stronger evidence;
|
|
67
|
+
- `incidental`: test-only, containment, or weak-confidence dependencies.
|
|
68
|
+
|
|
69
|
+
Local risk scoring adds bounded `semanticRiskPoints` for critical traversed
|
|
70
|
+
dependencies and contracts in the changed files. The output lists the exact
|
|
71
|
+
assertions and the formula; callers never receive a hidden semantic multiplier.
|
|
72
|
+
Large impact traversals cap returned assertions at 2,000 (repository manifests
|
|
73
|
+
at 5,000) and report total/returned counts in `semantic.truncation`; risk uses
|
|
74
|
+
the complete pre-truncation counts.
|
|
75
|
+
|
|
76
|
+
## Hybrid provenance
|
|
77
|
+
|
|
78
|
+
Hybrid queries preserve hosted canonical assertions and local checkout-delta
|
|
79
|
+
assertions under `snipara.semantic.hybrid.v1`. Snipara unions the evidence but
|
|
80
|
+
does not manufacture cross-source edges or assertions.
|
|
81
|
+
|
|
82
|
+
## Historical regression paths
|
|
83
|
+
|
|
84
|
+
Hosted impact can associate sanitized warning/error execution traces with
|
|
85
|
+
graph paths by their tracked files. Version 1 reports sample count, an
|
|
86
|
+
uncalibrated shadow association strength, event IDs, and a suggested risk
|
|
87
|
+
delta. This surface is intentionally
|
|
88
|
+
`mode: shadow` and `riskContributionEnabled: false`: temporal association is
|
|
89
|
+
not causal proof, and it cannot affect risk until separately calibrated and
|
|
90
|
+
promoted.
|
|
91
|
+
|
|
92
|
+
The local overlay has no durable outcome stream, so it returns the same shadow
|
|
93
|
+
contract with zero samples and directs callers to hosted evidence when needed.
|
|
94
|
+
|
|
95
|
+
## Example
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx -y snipara-companion code impact \
|
|
99
|
+
--source local \
|
|
100
|
+
--changed-files src/auth/session-repository.ts \
|
|
101
|
+
--depth 4 \
|
|
102
|
+
--json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Inspect `result.semantic.assertions`, `result.semantic.summary`, and
|
|
106
|
+
`result.risk.semanticRiskPoints` in the response.
|