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 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 === 2 && previousFile?.sha256 === contentHash2 && previousFile.language === language;
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: 2,
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 rawScore = criticality * (directFiles.size * 8 + propagation * 4) + uncertaintyPenalty;
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
- risk: buildLocalRisk(manifest, changedFiles, traversal, depth)
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snipara-companion",
3
- "version": "3.6.1",
3
+ "version": "3.6.3",
4
4
  "description": "Local-first CLI that asks your repo what breaks before an AI coding agent edits it.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {