snipara-companion 3.6.1 → 3.6.2

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