@neurcode-ai/cli 0.18.0 → 0.19.0

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.
Files changed (41) hide show
  1. package/dist/commands/brain.d.ts.map +1 -1
  2. package/dist/commands/brain.js +451 -0
  3. package/dist/commands/brain.js.map +1 -1
  4. package/dist/commands/policy.d.ts.map +1 -1
  5. package/dist/commands/policy.js +296 -0
  6. package/dist/commands/policy.js.map +1 -1
  7. package/dist/commands/runtime-adapter.d.ts +2 -1
  8. package/dist/commands/runtime-adapter.d.ts.map +1 -1
  9. package/dist/commands/runtime-adapter.js +51 -2
  10. package/dist/commands/runtime-adapter.js.map +1 -1
  11. package/dist/commands/session-hook.d.ts +13 -0
  12. package/dist/commands/session-hook.d.ts.map +1 -1
  13. package/dist/commands/session-hook.js +115 -15
  14. package/dist/commands/session-hook.js.map +1 -1
  15. package/dist/runtime-build.json +4 -4
  16. package/dist/utils/agent-adapter-setup.js +1 -1
  17. package/dist/utils/agent-adapter-setup.js.map +1 -1
  18. package/dist/utils/agent-guard.d.ts +1 -0
  19. package/dist/utils/agent-guard.d.ts.map +1 -1
  20. package/dist/utils/agent-guard.js +18 -6
  21. package/dist/utils/agent-guard.js.map +1 -1
  22. package/dist/utils/git-coverage.d.ts.map +1 -1
  23. package/dist/utils/git-coverage.js +1 -0
  24. package/dist/utils/git-coverage.js.map +1 -1
  25. package/dist/utils/local-repo-brain.d.ts +7 -0
  26. package/dist/utils/local-repo-brain.d.ts.map +1 -1
  27. package/dist/utils/local-repo-brain.js +18 -5
  28. package/dist/utils/local-repo-brain.js.map +1 -1
  29. package/dist/utils/proposed-change-analysis.d.ts +20 -0
  30. package/dist/utils/proposed-change-analysis.d.ts.map +1 -0
  31. package/dist/utils/proposed-change-analysis.js +448 -0
  32. package/dist/utils/proposed-change-analysis.js.map +1 -0
  33. package/dist/utils/repo-intelligence-v2.d.ts +28 -0
  34. package/dist/utils/repo-intelligence-v2.d.ts.map +1 -0
  35. package/dist/utils/repo-intelligence-v2.js +174 -0
  36. package/dist/utils/repo-intelligence-v2.js.map +1 -0
  37. package/dist/utils/v0-governance.d.ts +1 -1
  38. package/dist/utils/v0-governance.d.ts.map +1 -1
  39. package/dist/utils/v0-governance.js +49 -17
  40. package/dist/utils/v0-governance.js.map +1 -1
  41. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"brain.d.ts","sourceRoot":"","sources":["../../src/commands/brain.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgZpC,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgiDnD"}
1
+ {"version":3,"file":"brain.d.ts","sourceRoot":"","sources":["../../src/commands/brain.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsfpC,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA46DnD"}
@@ -45,6 +45,7 @@ exports.brainCommand = brainCommand;
45
45
  const child_process_1 = require("child_process");
46
46
  const fs_1 = require("fs");
47
47
  const path_1 = require("path");
48
+ const brain_1 = require("@neurcode-ai/brain");
48
49
  const brain_cache_1 = require("../utils/brain-cache");
49
50
  const local_repo_brain_1 = require("../utils/local-repo-brain");
50
51
  const repo_brain_impact_1 = require("../utils/repo-brain-impact");
@@ -57,6 +58,7 @@ const messages_1 = require("../utils/messages");
57
58
  const brain_context_1 = require("../utils/brain-context");
58
59
  const semantic_1 = require("../semantic");
59
60
  const ask_cache_1 = require("../utils/ask-cache");
61
+ const proposed_change_analysis_1 = require("../utils/proposed-change-analysis");
60
62
  // Import chalk with fallback
61
63
  let chalk;
62
64
  try {
@@ -280,6 +282,86 @@ function splitChangedPathList(value) {
280
282
  .map((path) => path.trim())
281
283
  .filter(Boolean);
282
284
  }
285
+ function parseRenameList(value) {
286
+ return splitChangedPathList(value)
287
+ .map((entry) => {
288
+ const delimiter = entry.includes('=>') ? '=>' : ':';
289
+ const [from, to] = entry.split(delimiter, 2).map((item) => item.trim());
290
+ return from && to ? { from, to } : null;
291
+ })
292
+ .filter((entry) => Boolean(entry));
293
+ }
294
+ function repositoryGraphError(error, json) {
295
+ const locked = error instanceof brain_1.RepositoryGraphLockedError;
296
+ const payload = {
297
+ ok: false,
298
+ code: locked ? 'repository_graph_locked' : 'repository_graph_failed',
299
+ message: error instanceof Error ? error.message : String(error),
300
+ exitCode: locked ? 3 : 1,
301
+ };
302
+ if (json) {
303
+ console.log(JSON.stringify(payload, null, 2));
304
+ }
305
+ else {
306
+ (0, messages_1.printError)('Repository Graph V2 operation failed', payload.message);
307
+ }
308
+ process.exitCode = payload.exitCode;
309
+ }
310
+ function printRepositoryGraphIndexResult(repoRoot, result, json) {
311
+ const payload = {
312
+ ok: true,
313
+ graphPath: (0, brain_1.repositoryGraphPath)(repoRoot),
314
+ schemaVersion: result.graph.schemaVersion,
315
+ graphId: result.graph.graphId,
316
+ generation: result.graph.generation,
317
+ freshness: result.graph.freshness,
318
+ coverage: result.graph.coverage,
319
+ stats: result.stats,
320
+ privacy: result.graph.privacy,
321
+ };
322
+ if (json) {
323
+ console.log(JSON.stringify(payload, null, 2));
324
+ return;
325
+ }
326
+ console.log(chalk.bold('\n🧠 Repository Graph V2\n'));
327
+ console.log(chalk.dim(`Mode: ${result.stats.mode}`));
328
+ console.log(chalk.dim(`Generation: ${result.graph.generation}`));
329
+ console.log(chalk.dim(`Freshness: ${result.graph.freshness.state}`));
330
+ console.log(chalk.dim(`Files indexed: ${result.graph.coverage.filesIndexed}`));
331
+ console.log(chalk.dim(`Files parsed: ${result.stats.filesParsed}`));
332
+ console.log(chalk.dim(`Files reused: ${result.stats.filesReused}`));
333
+ console.log(chalk.dim(`Unsupported: ${result.graph.coverage.unsupportedPercent}%`));
334
+ console.log(chalk.dim(`Nodes / edges: ${result.graph.nodes.length} / ${result.graph.edges.length}`));
335
+ console.log(chalk.dim(`Graph size: ${formatBytes(result.stats.graphBytes)}`));
336
+ console.log(chalk.dim(`Duration: ${result.stats.durationMs}ms`));
337
+ console.log(chalk.dim(`Peak RSS: ${result.stats.peakMemoryMb} MB`));
338
+ console.log(chalk.dim(`Artifact: ${payload.graphPath}`));
339
+ console.log(chalk.dim('Privacy: source-free local structural facts; raw source is not retained.'));
340
+ }
341
+ function normalizeAdvisoryTarget(repoRoot, input) {
342
+ const absolutePath = (0, path_1.isAbsolute)(input) ? (0, path_1.resolve)(input) : (0, path_1.resolve)(repoRoot, input);
343
+ const relativePath = (0, path_1.relative)(repoRoot, absolutePath).replace(/\\/g, '/');
344
+ if (!relativePath || relativePath === '..' || relativePath.startsWith('../')) {
345
+ throw new Error(`Path is outside repository root: ${input}`);
346
+ }
347
+ return { relativePath, absolutePath };
348
+ }
349
+ function advisoryCategory(value) {
350
+ const categories = [
351
+ 'behavior_similarity',
352
+ 'reuse_suggestion',
353
+ 'duplicate_module',
354
+ 'architecture_deviation',
355
+ 'cross_service_consequence',
356
+ 'reviewer_question',
357
+ 'missing_test',
358
+ 'ownership_review',
359
+ ];
360
+ if (!categories.includes(value)) {
361
+ throw new Error(`Unsupported advisory category: ${value}`);
362
+ }
363
+ return value;
364
+ }
283
365
  function renderBrainExportMarkdown(input) {
284
366
  const lines = [];
285
367
  lines.push('# Neurcode Brain Export');
@@ -595,6 +677,375 @@ function brainCommand(program) {
595
677
  }
596
678
  (0, messages_1.printInfo)('Next', 'Run: neurcode brain inspect "<area or symbol>"');
597
679
  });
680
+ // -- Repository Graph V2 ---------------------------------------------------
681
+ brain
682
+ .command('repo-index')
683
+ .description('Create or incrementally refresh the persistent Repository Graph V2')
684
+ .option('--changed <paths>', 'Changed paths separated by commas, spaces, or newlines')
685
+ .option('--deleted <paths>', 'Deleted paths separated by commas, spaces, or newlines')
686
+ .option('--rename <pairs>', 'Rename pairs as old:new or old=>new, separated by commas')
687
+ .option('--max-files <n>', 'Maximum files to inspect', (value) => parseInt(value, 10))
688
+ .option('--max-total-bytes <n>', 'Maximum total bytes to inspect', (value) => parseInt(value, 10))
689
+ .option('--max-bytes-per-file <n>', 'Maximum bytes per file', (value) => parseInt(value, 10))
690
+ .option('--json', 'Output stable machine-readable JSON')
691
+ .action(async (options) => {
692
+ const scope = getBrainScope();
693
+ try {
694
+ const limits = {
695
+ ...(Number.isFinite(options.maxFiles) ? { maxFiles: options.maxFiles } : {}),
696
+ ...(Number.isFinite(options.maxTotalBytes) ? { maxTotalBytes: options.maxTotalBytes } : {}),
697
+ ...(Number.isFinite(options.maxBytesPerFile) ? { maxBytesPerFile: options.maxBytesPerFile } : {}),
698
+ };
699
+ const result = await (0, brain_1.indexRepositoryGraph)({
700
+ repoRoot: scope.cwd,
701
+ changedPaths: splitChangedPathList(options.changed),
702
+ deletedPaths: splitChangedPathList(options.deleted),
703
+ renamedPaths: parseRenameList(options.rename),
704
+ limits,
705
+ });
706
+ printRepositoryGraphIndexResult(scope.cwd, result, options.json);
707
+ }
708
+ catch (error) {
709
+ repositoryGraphError(error, options.json);
710
+ }
711
+ });
712
+ brain
713
+ .command('repo-refresh')
714
+ .description('Refresh stale Repository Graph V2 files using content hashes')
715
+ .option('--changed <paths>', 'Optional changed paths separated by commas, spaces, or newlines')
716
+ .option('--deleted <paths>', 'Optional deleted paths separated by commas, spaces, or newlines')
717
+ .option('--rename <pairs>', 'Optional rename pairs as old:new or old=>new')
718
+ .option('--json', 'Output stable machine-readable JSON')
719
+ .action(async (options) => {
720
+ const scope = getBrainScope();
721
+ try {
722
+ const result = await (0, brain_1.indexRepositoryGraph)({
723
+ repoRoot: scope.cwd,
724
+ changedPaths: splitChangedPathList(options.changed),
725
+ deletedPaths: splitChangedPathList(options.deleted),
726
+ renamedPaths: parseRenameList(options.rename),
727
+ });
728
+ printRepositoryGraphIndexResult(scope.cwd, result, options.json);
729
+ }
730
+ catch (error) {
731
+ repositoryGraphError(error, options.json);
732
+ }
733
+ });
734
+ brain
735
+ .command('repo-status')
736
+ .description('Show Repository Graph V2 freshness, coverage, and parser depth')
737
+ .option('--json', 'Output stable machine-readable JSON')
738
+ .action(async (options) => {
739
+ const scope = getBrainScope();
740
+ try {
741
+ const freshness = await (0, brain_1.repositoryGraphStatus)(scope.cwd);
742
+ const graph = (0, brain_1.readRepositoryGraph)(scope.cwd);
743
+ const payload = {
744
+ ok: freshness.state !== 'corrupt',
745
+ repoRoot: scope.cwd,
746
+ graphPath: (0, brain_1.repositoryGraphPath)(scope.cwd),
747
+ freshness,
748
+ graph: graph ? {
749
+ schemaVersion: graph.schemaVersion,
750
+ graphId: graph.graphId,
751
+ generation: graph.generation,
752
+ updatedAt: graph.updatedAt,
753
+ coverage: graph.coverage,
754
+ nodeCount: graph.nodes.length,
755
+ edgeCount: graph.edges.length,
756
+ privacy: graph.privacy,
757
+ } : null,
758
+ };
759
+ if (options.json) {
760
+ console.log(JSON.stringify(payload, null, 2));
761
+ return;
762
+ }
763
+ console.log(chalk.bold('\n🧠 Repository Graph V2 Status\n'));
764
+ console.log(chalk.dim(`State: ${freshness.state}`));
765
+ console.log(chalk.dim(`Indexed at: ${freshness.indexedAt ?? 'never'}`));
766
+ console.log(chalk.dim(`Stale files: ${freshness.staleFileCount}`));
767
+ console.log(chalk.dim(`Unsupported files: ${freshness.unsupportedFileCount}`));
768
+ console.log(chalk.dim(`Reason codes: ${freshness.reasonCodes.join(', ') || 'none'}`));
769
+ if (graph) {
770
+ console.log(chalk.dim(`Generation: ${graph.generation}`));
771
+ console.log(chalk.dim(`Nodes / edges: ${graph.nodes.length} / ${graph.edges.length}`));
772
+ console.log(chalk.dim(`Unsupported: ${graph.coverage.unsupportedPercent}%`));
773
+ for (const language of graph.coverage.languages) {
774
+ console.log(chalk.dim(` ${language.language}: ${language.depth}; ${language.filesAnalyzed}/${language.filesSeen} analyzed`));
775
+ }
776
+ }
777
+ else {
778
+ console.log(chalk.dim('Run `neurcode brain repo-index` to create the graph.'));
779
+ }
780
+ }
781
+ catch (error) {
782
+ repositoryGraphError(error, options.json);
783
+ }
784
+ });
785
+ brain
786
+ .command('repo-explain <query...>')
787
+ .description('Explain a source-free path, symbol, package, service, or surface in Repository Graph V2')
788
+ .option('--limit <n>', 'Maximum matching nodes (default: 25)', (value) => parseInt(value, 10))
789
+ .option('--json', 'Output stable machine-readable JSON')
790
+ .action((queryParts, options) => {
791
+ const scope = getBrainScope();
792
+ const graph = (0, brain_1.readRepositoryGraph)(scope.cwd);
793
+ if (!graph) {
794
+ repositoryGraphError(new Error('Repository Graph V2 is missing or corrupt. Run `neurcode brain repo-index`.'), options.json);
795
+ return;
796
+ }
797
+ const query = queryParts.join(' ').trim();
798
+ const matches = (0, brain_1.explainRepositoryGraph)(graph, query, options.limit);
799
+ const matchIds = new Set(matches.map((node) => node.id));
800
+ const edges = graph.edges.filter((edge) => matchIds.has(edge.fromId) || matchIds.has(edge.toId));
801
+ const payload = { ok: true, query, matches, edges, freshness: graph.freshness };
802
+ if (options.json) {
803
+ console.log(JSON.stringify(payload, null, 2));
804
+ return;
805
+ }
806
+ console.log(chalk.bold(`\n🧠 Repository Graph V2: ${query}\n`));
807
+ if (matches.length === 0) {
808
+ console.log(chalk.dim('No source-free graph facts matched.'));
809
+ return;
810
+ }
811
+ matches.forEach((node, index) => {
812
+ console.log(chalk.white(` ${index + 1}. [${node.kind}] ${node.name ?? node.key}`));
813
+ if (node.path)
814
+ console.log(chalk.dim(` path: ${node.path}`));
815
+ console.log(chalk.dim(` parser: ${node.provenance.parserDepth}`));
816
+ });
817
+ console.log(chalk.dim(`Related edges: ${edges.length}`));
818
+ });
819
+ brain
820
+ .command('repo-query')
821
+ .description('Query deterministic Repository Graph V2 references, dependencies, imports, calls, tests, or boundaries')
822
+ .option('--path <path>', 'Seed path or path fragment')
823
+ .option('--symbol <name>', 'Seed symbol name or fragment')
824
+ .option('--relationship <type>', 'Edge type such as references, depends_on, imports, calls, tests, or crosses_boundary')
825
+ .option('--direction <direction>', 'in | out | both', 'both')
826
+ .option('--limit <n>', 'Maximum nodes and edges (default: 100)', (value) => parseInt(value, 10))
827
+ .option('--json', 'Output stable machine-readable JSON')
828
+ .action((options) => {
829
+ const scope = getBrainScope();
830
+ const graph = (0, brain_1.readRepositoryGraph)(scope.cwd);
831
+ if (!graph) {
832
+ repositoryGraphError(new Error('Repository Graph V2 is missing or corrupt. Run `neurcode brain repo-index`.'), options.json);
833
+ return;
834
+ }
835
+ const allowedRelationships = new Set([
836
+ 'defines', 'references', 'imports', 'exports', 'calls', 'owns',
837
+ 'belongs_to_package', 'belongs_to_service', 'tests', 'depends_on',
838
+ 'structurally_resembles', 'crosses_boundary',
839
+ ]);
840
+ const relationship = options.relationship;
841
+ if (relationship && !allowedRelationships.has(relationship)) {
842
+ repositoryGraphError(new Error(`Unsupported relationship: ${relationship}`), options.json);
843
+ return;
844
+ }
845
+ const direction = options.direction === 'in' || options.direction === 'out' ? options.direction : 'both';
846
+ const result = (0, brain_1.queryRepositoryGraph)(graph, {
847
+ path: options.path,
848
+ symbol: options.symbol,
849
+ relationship,
850
+ direction,
851
+ limit: options.limit,
852
+ });
853
+ const payload = { ok: true, query: options, ...result, freshness: graph.freshness };
854
+ if (options.json) {
855
+ console.log(JSON.stringify(payload, null, 2));
856
+ return;
857
+ }
858
+ console.log(chalk.bold('\n🧠 Repository Graph V2 Query\n'));
859
+ console.log(chalk.dim(`Seeds: ${result.seeds.length} | Nodes: ${result.nodes.length} | Edges: ${result.edges.length}`));
860
+ result.edges.forEach((edge) => {
861
+ console.log(chalk.dim(` ${edge.type}: ${edge.fromId} -> ${edge.toId}`));
862
+ });
863
+ });
864
+ brain
865
+ .command('repo-rebuild')
866
+ .description('Rebuild Repository Graph V2 from local repository state')
867
+ .option('--json', 'Output stable machine-readable JSON')
868
+ .action(async (options) => {
869
+ const scope = getBrainScope();
870
+ try {
871
+ const result = await (0, brain_1.indexRepositoryGraph)({ repoRoot: scope.cwd, forceRebuild: true });
872
+ printRepositoryGraphIndexResult(scope.cwd, result, options.json);
873
+ }
874
+ catch (error) {
875
+ repositoryGraphError(error, options.json);
876
+ }
877
+ });
878
+ brain
879
+ .command('repo-recover')
880
+ .description('Recover Repository Graph V2 from its last atomic backup or rebuild when unavailable')
881
+ .option('--json', 'Output stable machine-readable JSON')
882
+ .action(async (options) => {
883
+ const scope = getBrainScope();
884
+ try {
885
+ const result = await (0, brain_1.recoverRepositoryGraph)(scope.cwd);
886
+ printRepositoryGraphIndexResult(scope.cwd, result, options.json);
887
+ }
888
+ catch (error) {
889
+ repositoryGraphError(error, options.json);
890
+ }
891
+ });
892
+ brain
893
+ .command('advisory <path>')
894
+ .description('Run local-only advisory semantic intelligence for a proposed or current file')
895
+ .option('--content-file <path>', 'Local proposed content file; raw content is parsed locally and not retained')
896
+ .option('--path-only', 'Run without proposed content and expose coverage limitations')
897
+ .option('--include-suppressed', 'Include findings suppressed by local feedback')
898
+ .option('--no-index', 'Do not create or refresh Repository Graph V2')
899
+ .option('--json', 'Output stable source-free JSON')
900
+ .action(async (path, options) => {
901
+ const scope = getBrainScope();
902
+ try {
903
+ let graph = (0, brain_1.readRepositoryGraph)(scope.cwd);
904
+ const status = await (0, brain_1.repositoryGraphStatus)(scope.cwd);
905
+ if (options.index !== false && (!graph || status.state !== 'fresh')) {
906
+ graph = (await (0, brain_1.indexRepositoryGraph)({ repoRoot: scope.cwd })).graph;
907
+ }
908
+ else if (graph) {
909
+ graph = { ...graph, freshness: status };
910
+ }
911
+ if (!graph)
912
+ throw new Error('Repository Graph V2 is missing. Run `neurcode brain repo-index`.');
913
+ const target = normalizeAdvisoryTarget(scope.cwd, path);
914
+ let proposedSource = null;
915
+ let sourceKind = 'not_available';
916
+ if (!options.pathOnly) {
917
+ const contentPath = options.contentFile
918
+ ? ((0, path_1.isAbsolute)(options.contentFile) ? options.contentFile : (0, path_1.resolve)(scope.cwd, options.contentFile))
919
+ : target.absolutePath;
920
+ if ((0, fs_1.existsSync)(contentPath)) {
921
+ proposedSource = (0, fs_1.readFileSync)(contentPath, 'utf8');
922
+ sourceKind = options.contentFile ? 'write_content' : 'post_write_disk_read';
923
+ }
924
+ }
925
+ const analysis = (0, proposed_change_analysis_1.analyzeProposedChange)({
926
+ repoRoot: scope.cwd,
927
+ filePath: target.relativePath,
928
+ proposedSource,
929
+ sourceKind,
930
+ adapterId: 'neurcode-cli',
931
+ timing: sourceKind === 'post_write_disk_read' ? 'after_write' : 'before_write',
932
+ sessionId: null,
933
+ planRevision: null,
934
+ });
935
+ analysis.envelope.target.operation = (0, fs_1.existsSync)(target.absolutePath) ? 'update' : 'create';
936
+ const result = await (0, brain_1.runSemanticAdvisory)({
937
+ repoRoot: scope.cwd,
938
+ graph,
939
+ change: analysis.envelope,
940
+ });
941
+ const findings = options.includeSuppressed
942
+ ? result.findings
943
+ : result.findings.filter((finding) => !finding.suppressed);
944
+ const payload = {
945
+ ok: true,
946
+ truth: 'advisory',
947
+ blocking: false,
948
+ cacheHit: result.cacheHit,
949
+ cacheKey: result.cacheKey,
950
+ graph: {
951
+ graphId: graph.graphId,
952
+ generation: graph.generation,
953
+ freshness: graph.freshness,
954
+ },
955
+ host: analysis.envelope.host,
956
+ findings,
957
+ suppressedCount: result.findings.filter((finding) => finding.suppressed).length,
958
+ privacy: analysis.envelope.privacy,
959
+ };
960
+ if (options.json) {
961
+ console.log(JSON.stringify(payload, null, 2));
962
+ return;
963
+ }
964
+ console.log(chalk.bold('\n🧠 Advisory Semantic Intelligence V2\n'));
965
+ console.log(chalk.dim('Truth: advisory · Blocking: never'));
966
+ console.log(chalk.dim(`Graph: ${graph.graphId} generation ${graph.generation} (${graph.freshness.state})`));
967
+ console.log(chalk.dim(`Cache: ${result.cacheHit ? 'hit' : 'miss'}`));
968
+ if (findings.length === 0) {
969
+ console.log(chalk.dim('No unsuppressed advisory findings.'));
970
+ return;
971
+ }
972
+ findings.forEach((finding, index) => {
973
+ console.log(chalk.white(` ${index + 1}. ${finding.category} · ${(finding.confidence * 100).toFixed(0)}%`));
974
+ console.log(chalk.dim(` ${finding.rationaleCategories.join(', ')}`));
975
+ console.log(chalk.dim(` ${finding.related.map((item) => item.path || item.symbol || item.hash).filter(Boolean).join(', ')}`));
976
+ console.log(chalk.dim(` limitations: ${finding.limitations.join(' ')}`));
977
+ console.log(chalk.dim(` id: ${finding.findingId}`));
978
+ });
979
+ }
980
+ catch (error) {
981
+ repositoryGraphError(error, options.json);
982
+ }
983
+ });
984
+ brain
985
+ .command('advisory-suppress <key>')
986
+ .description('Suppress a finding ID, category:<name>, path:<path>, or fingerprint:<hash> locally')
987
+ .option('--remove', 'Remove the suppression instead')
988
+ .option('--json', 'Output stable source-free JSON')
989
+ .action((key, options) => {
990
+ const scope = getBrainScope();
991
+ try {
992
+ const registry = new brain_1.SemanticAdvisoryRegistry(scope.cwd);
993
+ const status = options.remove ? registry.unsuppress(key) : registry.suppress(key);
994
+ if (options.json)
995
+ console.log(JSON.stringify({ ok: true, status }, null, 2));
996
+ else
997
+ console.log(chalk.green(`\n✅ Advisory suppression ${options.remove ? 'removed' : 'saved'}: ${key}\n`));
998
+ }
999
+ catch (error) {
1000
+ repositoryGraphError(error, options.json);
1001
+ }
1002
+ });
1003
+ brain
1004
+ .command('advisory-feedback <finding-id>')
1005
+ .description('Record source-free advisory feedback: accepted, dismissed, duplicate, or useful')
1006
+ .requiredOption('--category <category>', 'Advisory category')
1007
+ .requiredOption('--outcome <outcome>', 'accepted | dismissed | duplicate | useful')
1008
+ .option('--json', 'Output stable source-free JSON')
1009
+ .action((findingId, options) => {
1010
+ const scope = getBrainScope();
1011
+ try {
1012
+ const outcomes = ['accepted', 'dismissed', 'duplicate', 'useful'];
1013
+ if (!outcomes.includes(options.outcome)) {
1014
+ throw new Error(`Unsupported advisory feedback outcome: ${options.outcome}`);
1015
+ }
1016
+ const registry = new brain_1.SemanticAdvisoryRegistry(scope.cwd);
1017
+ const status = registry.recordFeedback({
1018
+ findingId,
1019
+ category: advisoryCategory(options.category),
1020
+ outcome: options.outcome,
1021
+ });
1022
+ if (options.json)
1023
+ console.log(JSON.stringify({ ok: true, status }, null, 2));
1024
+ else
1025
+ console.log(chalk.green(`\n✅ Advisory feedback recorded: ${options.outcome}\n`));
1026
+ }
1027
+ catch (error) {
1028
+ repositoryGraphError(error, options.json);
1029
+ }
1030
+ });
1031
+ brain
1032
+ .command('advisory-status')
1033
+ .description('Show local advisory cache, suppressions, feedback, and privacy mode')
1034
+ .option('--json', 'Output stable source-free JSON')
1035
+ .action((options) => {
1036
+ const scope = getBrainScope();
1037
+ const status = new brain_1.SemanticAdvisoryRegistry(scope.cwd).status();
1038
+ if (options.json) {
1039
+ console.log(JSON.stringify({ ok: true, status }, null, 2));
1040
+ return;
1041
+ }
1042
+ console.log(chalk.bold('\n🧠 Advisory Semantic Intelligence Status\n'));
1043
+ console.log(chalk.dim(`Registry: ${status.path}`));
1044
+ console.log(chalk.dim(`Cache entries: ${status.cacheEntries}`));
1045
+ console.log(chalk.dim(`Suppressions: ${status.suppressions.length}`));
1046
+ console.log(chalk.dim(`Feedback IDs: ${Object.keys(status.feedback).length}`));
1047
+ console.log(chalk.dim('Privacy: local-only; no source, diff, prompt, or chat upload.'));
1048
+ });
598
1049
  // -- brain inspect ----------------------------------------------------------
599
1050
  brain
600
1051
  .command('inspect [query...]')