@launchsecure/launch-kit 0.0.30 → 0.0.31

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 (102) hide show
  1. package/dist/beacon/beacon.mjs +934 -865
  2. package/dist/beacon/beacon.mjs.map +1 -1
  3. package/dist/beacon/beacon.umd.js +9 -9
  4. package/dist/beacon/beacon.umd.js.map +1 -1
  5. package/dist/beacon/types/internal/screenshot.d.ts +19 -1
  6. package/dist/beacon/types/internal/screenshot.d.ts.map +1 -1
  7. package/dist/beacon/types/plugins/domEle.d.ts.map +1 -1
  8. package/dist/chart-client/assets/{index-CJ4mgRRF.css → index-CDIhdgWg.css} +1 -1
  9. package/dist/chart-client/index.html +2 -2
  10. package/dist/client/assets/{index-DI5qSR_w.css → index-CfW4n40I.css} +1 -1
  11. package/dist/client/index.html +2 -2
  12. package/dist/council-client/assets/{index-C_-vAM9L.css → index-CZim6x1u.css} +1 -1
  13. package/dist/council-client/index.html +2 -2
  14. package/dist/deck-client/assets/{_baseUniq-DCt2IMRR.js → _baseUniq-DdHaBFYO.js} +1 -1
  15. package/dist/deck-client/assets/{arc-h-ifqmNR.js → arc-D98e_18X.js} +1 -1
  16. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-C9dITSPv.js → architectureDiagram-Q4EWVU46-DNFZzh-4.js} +1 -1
  17. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BHuJT34t.js → blockDiagram-DXYQGD6D-DeQvGUdX.js} +1 -1
  18. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CpvMGtDG.js → c4Diagram-AHTNJAMY-B6ekZf1n.js} +1 -1
  19. package/dist/deck-client/assets/channel-DmR7Tyyt.js +1 -0
  20. package/dist/deck-client/assets/{chunk-4BX2VUAB-B6md1VIm.js → chunk-4BX2VUAB-9aDWymq2.js} +1 -1
  21. package/dist/deck-client/assets/{chunk-4TB4RGXK-BmEnX8ik.js → chunk-4TB4RGXK-DtKQqaI7.js} +1 -1
  22. package/dist/deck-client/assets/{chunk-55IACEB6-BZPUyZAZ.js → chunk-55IACEB6-COy9hEae.js} +1 -1
  23. package/dist/deck-client/assets/{chunk-EDXVE4YY-BWwNUK-l.js → chunk-EDXVE4YY-D_f861An.js} +1 -1
  24. package/dist/deck-client/assets/{chunk-FMBD7UC4-o7gSppGI.js → chunk-FMBD7UC4-CmuA5UKn.js} +1 -1
  25. package/dist/deck-client/assets/{chunk-OYMX7WX6-C4KoTL5p.js → chunk-OYMX7WX6-vT8z8D-0.js} +1 -1
  26. package/dist/deck-client/assets/{chunk-QZHKN3VN-jkf68sDs.js → chunk-QZHKN3VN-CTlwwg-R.js} +1 -1
  27. package/dist/deck-client/assets/{chunk-YZCP3GAM-Cd4yBE7o.js → chunk-YZCP3GAM-C44yr620.js} +1 -1
  28. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-Bl4ozQWs.js +1 -0
  29. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-Bl4ozQWs.js +1 -0
  30. package/dist/deck-client/assets/clone-BAy58j24.js +1 -0
  31. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-DeGFUgAV.js → cose-bilkent-S5V4N54A-DBB2J2nL.js} +1 -1
  32. package/dist/deck-client/assets/{dagre-KV5264BT-ekcYJuUV.js → dagre-KV5264BT-DxDTYbKl.js} +1 -1
  33. package/dist/deck-client/assets/{diagram-5BDNPKRD-YHPk4rV2.js → diagram-5BDNPKRD-DByWrWd1.js} +1 -1
  34. package/dist/deck-client/assets/{diagram-G4DWMVQ6-DM-JCd_B.js → diagram-G4DWMVQ6-B8B6ddMq.js} +1 -1
  35. package/dist/deck-client/assets/{diagram-MMDJMWI5-l5FK1ybk.js → diagram-MMDJMWI5-BMUZ2PWK.js} +1 -1
  36. package/dist/deck-client/assets/{diagram-TYMM5635-CIN4_1-j.js → diagram-TYMM5635-Bk9e8BB-.js} +1 -1
  37. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-MyinSkEl.js → erDiagram-SMLLAGMA-DcOSwSol.js} +1 -1
  38. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-Dk8nn42x.js → flowDiagram-DWJPFMVM-DI-4BR0F.js} +1 -1
  39. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-BU1ihicu.js → ganttDiagram-T4ZO3ILL-BeZuXBoU.js} +1 -1
  40. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-BjsTL13C.js → gitGraphDiagram-UUTBAWPF-Bcki__f-.js} +1 -1
  41. package/dist/deck-client/assets/{graph-DJmh-xi7.js → graph-CifKx6G1.js} +1 -1
  42. package/dist/deck-client/assets/index-6sdqbm2o.js +2 -0
  43. package/dist/deck-client/assets/{index-DsIZ3LqL.css → index-BlTlhxFW.css} +1 -1
  44. package/dist/deck-client/assets/index-CB-qlwRT.js +1195 -0
  45. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-Dxvy_RB4.js → infoDiagram-42DDH7IO-CReN1nFN.js} +1 -1
  46. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DPOaNF1l.js → ishikawaDiagram-UXIWVN3A-CDF_VLN_.js} +1 -1
  47. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-DMew3K5c.js → journeyDiagram-VCZTEJTY-DwgGrNVB.js} +1 -1
  48. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-csciJFuk.js → kanban-definition-6JOO6SKY-DB_zohh5.js} +1 -1
  49. package/dist/deck-client/assets/{layout-Dg4yyms2.js → layout-DFfX1O3z.js} +1 -1
  50. package/dist/deck-client/assets/{linear-BA3zU6gq.js → linear-CtKb4EXj.js} +1 -1
  51. package/dist/deck-client/assets/{min-lz-Ird-p.js → min-DCRRwUZv.js} +1 -1
  52. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CCEN8OQV.js → mindmap-definition-QFDTVHPH-D0QBOiFe.js} +1 -1
  53. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DM6n1HY7.js → pieDiagram-DEJITSTG-CD-EV5WB.js} +1 -1
  54. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-_ULoR66n.js → quadrantDiagram-34T5L4WZ-B-JXZ8xI.js} +1 -1
  55. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BuwJs7Tn.js → requirementDiagram-MS252O5E-D2_OK5Dp.js} +1 -1
  56. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-BEsuzkW4.js → sankeyDiagram-XADWPNL6-BbBJqVSC.js} +1 -1
  57. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-CP2H0YWf.js → sequenceDiagram-FGHM5R23-Db8A-Rkk.js} +1 -1
  58. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-B5Gw_NNL.js → stateDiagram-FHFEXIEX-DGJnanjS.js} +1 -1
  59. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-CR7riiab.js +1 -0
  60. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DsoYydQa.js → timeline-definition-GMOUNBTQ-BRkr6T4w.js} +1 -1
  61. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-Dz8JT_ob.js → vennDiagram-DHZGUBPP-d0rsTqFo.js} +1 -1
  62. package/dist/deck-client/assets/wardley-RL74JXVD-2t7cMqdS.js +162 -0
  63. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-DN1LJMB1.js → wardleyDiagram-NUSXRM2D-DzboAsHh.js} +1 -1
  64. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-nb0oSfrQ.js → xychartDiagram-5P7HB3ND-CgTP9u2V.js} +1 -1
  65. package/dist/deck-client/index.html +2 -2
  66. package/dist/server/cli.js +666 -12
  67. package/dist/server/council-entry.js +0 -0
  68. package/dist/server/deck-mcp-entry.js +141 -21
  69. package/dist/server/deck-serve.js +141 -21
  70. package/dist/server/fb-wizard.js +0 -0
  71. package/dist/server/graph-mcp-entry.js +666 -12
  72. package/dist/server/init-entry.js +15 -9
  73. package/package.json +23 -21
  74. package/scaffolds/ls-marketplace/plugins/kit/skills/analyse/SKILL.md +180 -0
  75. package/scaffolds/ls-marketplace/plugins/kit/skills/{blast-radius.md → blast-radius/SKILL.md} +28 -12
  76. package/scaffolds/ls-marketplace/plugins/kit/skills/{debug.md → debug/SKILL.md} +2 -9
  77. package/scaffolds/ls-marketplace/plugins/kit/skills/{diagram.md → diagram/SKILL.md} +27 -9
  78. package/scaffolds/ls-marketplace/plugins/kit/skills/{prototype.md → prototype/SKILL.md} +21 -1
  79. package/scaffolds/ls-marketplace/plugins/kit/skills/recovery/SKILL.md +95 -0
  80. package/scaffolds/ls-marketplace/plugins/kit/skills/{wireframe.md → wireframe/SKILL.md} +21 -1
  81. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
  82. package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
  83. package/dist/deck-client/assets/channel-2PZVMiXf.js +0 -1
  84. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-Bt8xBAof.js +0 -1
  85. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-Bt8xBAof.js +0 -1
  86. package/dist/deck-client/assets/clone-BHQryoDl.js +0 -1
  87. package/dist/deck-client/assets/index-KsShfCV-.js +0 -476
  88. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-4T4wMDXr.js +0 -1
  89. package/dist/deck-client/assets/wardley-RL74JXVD-DGHQ_Ijv.js +0 -162
  90. package/scaffolds/ls-marketplace/plugins/kit/skills/recall.md +0 -83
  91. /package/dist/chart-client/assets/{index-Ccy-DpI-.js → index-B__ARB8k.js} +0 -0
  92. /package/dist/client/assets/{index-Dp0_okva.js → index-h8kMzVtG.js} +0 -0
  93. /package/dist/council-client/assets/{index-Dt4zWKSj.js → index-CWaDcsFR.js} +0 -0
  94. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-array.md → beacon-array/SKILL.md} +0 -0
  95. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-clear.md → beacon-clear/SKILL.md} +0 -0
  96. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-pulse.md → beacon-pulse/SKILL.md} +0 -0
  97. /package/scaffolds/ls-marketplace/plugins/kit/skills/{beacon-scan.md → beacon-scan/SKILL.md} +0 -0
  98. /package/scaffolds/ls-marketplace/plugins/kit/skills/{brief.md → brief/SKILL.md} +0 -0
  99. /package/scaffolds/ls-marketplace/plugins/kit/skills/{course.md → course/SKILL.md} +0 -0
  100. /package/scaffolds/ls-marketplace/plugins/kit/skills/{deploy-check.md → deploy-check/SKILL.md} +0 -0
  101. /package/scaffolds/ls-marketplace/plugins/kit/skills/{orbit.md → orbit/SKILL.md} +0 -0
  102. /package/scaffolds/ls-marketplace/plugins/kit/skills/{show-mcp-status.md → show-mcp-status/SKILL.md} +0 -0
@@ -33902,6 +33902,32 @@ var TOOLS = [
33902
33902
  type: "boolean",
33903
33903
  description: "DB layer only. When true, response includes `contradictions[]` and `flagged_edges[]` arrays from the SQL migrations parser \u2014 SQL\u2194ORM schema drift findings (missing columns/tables, type mismatches, nullability drift, FKs in SQL but no ORM @relation). Default false."
33904
33904
  },
33905
+ tag_predicates: {
33906
+ type: "array",
33907
+ description: 'Multi-condition AND filter over node attributes. Each predicate is { field, op, value }. `field` matches either a top-level node attribute (e.g. is_destructive, mutates, statement_count) OR a tag key (tag values take precedence when both exist). `op` \u2208 {eq, ne, gt, lt, gte, lte, exists, not_exists, in}. Example: [{field:"is_destructive",op:"eq",value:true},{field:"has_orphan_check",op:"eq",value:false}] returns destructive migrations missing an orphan check.',
33908
+ items: {
33909
+ type: "object",
33910
+ properties: {
33911
+ field: { type: "string", description: "Node attribute or tag key." },
33912
+ op: { type: "string", enum: ["eq", "ne", "gt", "lt", "gte", "lte", "exists", "not_exists", "in"] },
33913
+ value: { description: "Comparison value. Required for all ops except exists/not_exists. Pass an array for `in`." }
33914
+ },
33915
+ required: ["field", "op"]
33916
+ }
33917
+ },
33918
+ include_cross_refs: {
33919
+ type: "boolean",
33920
+ description: "When true, attach `cross_refs` (incoming + outgoing) to each returned node. Unlocks the cross-layer reference data (references_static, calls_api, reads_via, mutates_via, reads, mutates, references_api) that is otherwise dropped from responses. Combine with `cross_ref_type` to narrow."
33921
+ },
33922
+ cross_ref_type: {
33923
+ type: "string",
33924
+ description: "Narrow attached cross_refs by kind. Common values: calls_api, references_static, reads_via, mutates_via, reads, mutates, references_api. Only applies when include_cross_refs is true."
33925
+ },
33926
+ edge_confidence: {
33927
+ type: "array",
33928
+ description: 'Filter to surface low/medium/high-confidence flagged_edges in the response. Pass e.g. ["medium","high"] to get DYNAMIC navigation targets, FK drift, etc. Scoped to matched nodes when a node filter is in play; otherwise returns all flagged_edges at the requested confidence levels. Layer-independent.',
33929
+ items: { type: "string", enum: ["low", "medium", "high"] }
33930
+ },
33905
33931
  offset: {
33906
33932
  type: "number",
33907
33933
  description: "Skip first N matched nodes (pagination). Default 0. Use next_offset from a previous response to get the next page."
@@ -33925,6 +33951,10 @@ var TOOLS = [
33925
33951
  minimal: { type: "boolean" },
33926
33952
  include_edges: { type: "boolean" },
33927
33953
  include_findings: { type: "boolean" },
33954
+ tag_predicates: { type: "array" },
33955
+ include_cross_refs: { type: "boolean" },
33956
+ cross_ref_type: { type: "string" },
33957
+ edge_confidence: { type: "array" },
33928
33958
  project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
33929
33959
  worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
33930
33960
  project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
@@ -34023,12 +34053,16 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
34023
34053
  fields: {
34024
34054
  type: "array",
34025
34055
  items: { type: "string" },
34026
- description: 'Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params, effects. Omit for all. "effects" returns the file-level side-effect summary (calls, dom_writes, subscribes, timers, persists, fetches, globals); per-arrow-function effects are also attached to each entry in "variables".'
34056
+ description: `Specific deep fields to return. Options: elements, stateVars, conditions, variables, responses, params, effects, crossRefs. Omit for all. "effects" returns the file-level side-effect summary (calls, dom_writes, subscribes, timers, persists, fetches, globals); per-arrow-function effects are also attached to each entry in "variables". "crossRefs" returns this node's incoming + outgoing cross-layer references (calls_api, references_static, reads_via, mutates_via, reads, mutates, references_api) \u2014 the cross-layer linking data otherwise dropped from responses.`
34027
34057
  },
34028
34058
  filter: {
34029
34059
  type: "string",
34030
34060
  description: "Regex pattern to search WITHIN deep field values. Only returns nodes where at least one deep field matches. Searches across all string values in the requested fields (condition tests, variable inits, element tags/props, response bodies, etc.). When set, search becomes optional and node limit is raised to 50."
34031
34061
  },
34062
+ cross_ref_type: {
34063
+ type: "string",
34064
+ description: 'Narrow returned crossRefs by kind (e.g. calls_api, references_static, reads_via). Only applies when "crossRefs" is included in fields (or fields is omitted).'
34065
+ },
34032
34066
  case_insensitive: {
34033
34067
  type: "boolean",
34034
34068
  description: "Case-insensitive filter matching. Default true."
@@ -34073,18 +34107,18 @@ Use this when the user asks "is the chart running", "show me the project graph U
34073
34107
  },
34074
34108
  {
34075
34109
  name: "effects_index",
34076
- description: 'Cross-layer inverted index over per-node side effects. Answers "who else touches X?" without re-walking every node. Built automatically when generate_graph runs (or whenever the watcher regenerates). \n\nUSE THIS FOR: "is mountFoo safe to instantiate twice?" (kind="singleton_risks"), "who writes DOM id moon-shadow-blur?" (kind="dom_ids", key="moon-shadow-blur"), "which files attach a window keydown listener?" (kind="window_events", key="window:keydown"), "who else writes localStorage key panchang.settings.v1?" (kind="storage_keys", key="..."), "any DOM-id collisions?" (kind="collisions"). \n\nReturns: { kind, results } where results is a {key: [nodeIds]} map for the chosen kind, or a list of multi-writer collisions when kind="collisions", or a flat node-id list when kind="singleton_risks".',
34110
+ description: 'Cross-layer inverted index over per-node side effects. Answers "who else touches X?" without re-walking every node. Most kinds load from a precomputed effects-index.json (built when generate_graph runs); fetch_calls and api_effects are computed on demand from the ui/api graphs so they always reflect current state.\n\nUSE THIS FOR: "is mountFoo safe to instantiate twice?" (kind="singleton_risks"), "who writes DOM id moon-shadow-blur?" (kind="dom_ids", key="moon-shadow-blur"), "which files attach a window keydown listener?" (kind="window_events", key="window:keydown"), "who else writes localStorage key panchang.settings.v1?" (kind="storage_keys", key="..."), "any DOM-id collisions?" (kind="collisions"), "who calls /api/work-items?" (kind="fetch_calls", key="app/api/.../work-items/route.ts"), "which endpoints call db.user.findUnique?" (kind="api_effects", key="db.user.findUnique"). \n\nReturns vary by kind: {key:[nodeIds]} map for standard kinds; {dom_ids, storage_keys, window_events} for collisions; flat node-id list for singleton_risks; {api_endpoint|url_template: [ui_files]} for fetch_calls; {category: {item: [endpoint_ids]}} summary for api_effects (or {category, key, hits} when key is set).',
34077
34111
  inputSchema: {
34078
34112
  type: "object",
34079
34113
  properties: {
34080
34114
  kind: {
34081
34115
  type: "string",
34082
- enum: ["dom_ids", "window_events", "storage_keys", "fetch_urls", "timers", "singleton_risks", "collisions"],
34083
- description: "Which inverted index to query. Default: collisions (most actionable signal)."
34116
+ enum: ["dom_ids", "window_events", "storage_keys", "fetch_urls", "timers", "singleton_risks", "collisions", "fetch_calls", "api_effects"],
34117
+ description: 'Which inverted index to query. Default: collisions (most actionable signal). fetch_calls = "UI\u2192API call inventory" (resolved via ui cross_refs + raw fetch strings). api_effects = "what does each endpoint touch" inverted across all effect categories (calls / fetches / persists / subscribes / timers / dom_writes / globals).'
34084
34118
  },
34085
34119
  key: {
34086
34120
  type: "string",
34087
- description: 'Optional specific key to look up within the chosen kind (e.g. "moon-shadow-blur"). When omitted, returns the full {key:nodes} map for the kind.'
34121
+ description: 'Optional specific key to look up within the chosen kind (e.g. "moon-shadow-blur" for dom_ids, "/api/work-items" for fetch_calls, "db.user.findUnique" for api_effects). When omitted, returns the full {key:nodes} map (or per-category summary for api_effects).'
34088
34122
  },
34089
34123
  project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
34090
34124
  worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
@@ -34092,6 +34126,95 @@ Use this when the user asks "is the chart running", "show me the project graph U
34092
34126
  }
34093
34127
  }
34094
34128
  },
34129
+ {
34130
+ name: "migration_audit",
34131
+ description: `List Prisma migrations that violate the migration-safety discipline: destructive migrations missing one or more guards (orphan check, sidecar backup, pre-flight notice). Codifies the three-layer migration safety rules \u2014 derived from each migration node's extracted attributes (is_destructive, has_orphan_check, has_sidecar_backup, has_pre_flight_notice, contains_backfill, contains_drop_column, contains_drop_table).
34132
+
34133
+ USE THIS FOR: "are there unsafe migrations on this branch", "audit the migration history before deploy", "find migrations that drop columns without backup". Paginated. Returns one entry per offending migration with the specific missing guards listed.`,
34134
+ inputSchema: {
34135
+ type: "object",
34136
+ properties: {
34137
+ require_orphan_check: { type: "boolean", description: "Flag destructive migrations missing an orphan-count guard. Default true." },
34138
+ require_sidecar_backup: { type: "boolean", description: "Flag destructive migrations missing a sidecar backup table. Default true." },
34139
+ require_pre_flight_notice: { type: "boolean", description: "Flag destructive migrations missing a RAISE NOTICE pre-flight. Default true." },
34140
+ include_safe: { type: "boolean", description: "Also return migrations that pass all checks (no violations). Default false \u2014 by default only offenders are returned." },
34141
+ offset: { type: "number", description: "Pagination offset. Default 0." },
34142
+ limit: { type: "number", description: "Max migrations to return. Default 10." },
34143
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
34144
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
34145
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
34146
+ }
34147
+ }
34148
+ },
34149
+ {
34150
+ name: "drift_report",
34151
+ description: 'Aggregate cross-layer drift findings \u2014 contradictions (schema disagreements) + flagged_edges (low-confidence resolutions, FK drift, dynamic routes) \u2014 into one report. Generalizes the db-only `read_graph include_findings` flag to all layers.\n\nUSE THIS FOR: "is the codebase drifting", "show me SQL vs ORM mismatches", "list endpoints/components with unresolved routes", "audit schema health before refactor". Paginated. Returns items with { layer, kind, source, target, detail, confidence }.',
34152
+ inputSchema: {
34153
+ type: "object",
34154
+ properties: {
34155
+ layer: { type: "string", description: "Restrict to one layer (e.g. db, ui). Omit for all layers." },
34156
+ kind: { type: "string", enum: ["contradictions", "flagged_edges", "all"], description: "Which finding type to return. Default all." },
34157
+ confidence: { type: "array", items: { type: "string", enum: ["low", "medium", "high"] }, description: "For flagged_edges: filter by confidence levels. Default all." },
34158
+ offset: { type: "number", description: "Pagination offset. Default 0." },
34159
+ limit: { type: "number", description: "Max items to return. Default 10." },
34160
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
34161
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
34162
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
34163
+ }
34164
+ }
34165
+ },
34166
+ {
34167
+ name: "who_uses",
34168
+ description: 'List everything that references a target node via cross_refs \u2014 the cross-layer references_static / calls_api / reads_via / mutates_via / reads / mutates / references_api edges that are otherwise dropped from responses.\n\nUSE THIS FOR: "where is permission `manage_billing` used", "which UI components reference enum `WorkItemPriority`", "which endpoints touch the User table", "which files call /api/work-items". Auto-scans all layers\' cross_refs for incoming edges to the target. Paginated.',
34169
+ inputSchema: {
34170
+ type: "object",
34171
+ properties: {
34172
+ target: { type: "string", description: 'Target node id to look up (e.g. "seed:permission:manage_billing", "User", "app/api/.../work-items/route.ts", "WorkItemPriority").' },
34173
+ cross_ref_type: { type: "string", description: "Narrow by cross-ref kind (calls_api / references_static / reads_via / mutates_via / reads / mutates / references_api). Omit for all kinds." },
34174
+ offset: { type: "number", description: "Pagination offset. Default 0." },
34175
+ limit: { type: "number", description: "Max references to return. Default 10." },
34176
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
34177
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
34178
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
34179
+ },
34180
+ required: ["target"]
34181
+ }
34182
+ },
34183
+ {
34184
+ name: "trace_path",
34185
+ description: 'Find shortest paths between two nodes across layers via cross_refs (including the via[] chains that capture transitive DB access through middleware).\n\nUSE THIS FOR: "how does AdminAnalyticsPage end up reading the User table" \u2014 returns the call chain page \u2192 fetch \u2192 endpoint \u2192 middleware \u2192 db. BFS-bounded by max_hops + max_paths to keep responses tractable on hub nodes. Paginated.',
34186
+ inputSchema: {
34187
+ type: "object",
34188
+ properties: {
34189
+ from: { type: "string", description: "Source node id." },
34190
+ to: { type: "string", description: "Target node id." },
34191
+ max_hops: { type: "number", description: "Hard cap on path length. Default 5." },
34192
+ max_paths: { type: "number", description: "Hard cap on total paths discovered. Default 10. Once hit, BFS stops." },
34193
+ offset: { type: "number", description: "Pagination offset over discovered paths. Default 0." },
34194
+ limit: { type: "number", description: "Max paths returned per response. Default 10." },
34195
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
34196
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
34197
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
34198
+ },
34199
+ required: ["from", "to"]
34200
+ }
34201
+ },
34202
+ {
34203
+ name: "auth_coverage_report",
34204
+ description: 'Aggregate every API endpoint by its auth[] wrapper(s) \u2014 surfaces endpoints with empty auth, groups by module, shows which auth strategies dominate. Computed from api.json endpoint auth field (100% populated).\n\nUSE THIS FOR: "are there unauthenticated endpoints", "which auth wrappers are in use", "audit auth strategy consistency across modules". Paginated. Returns { total, by_strategy: {strategy: count}, unauthenticated: [endpoint_ids], by_module: {module: {total, by_strategy}} } plus the paginated endpoint list.',
34205
+ inputSchema: {
34206
+ type: "object",
34207
+ properties: {
34208
+ module: { type: "string", description: 'Restrict to one module tag (e.g. "orgs", "admin").' },
34209
+ strategy: { type: "string", description: 'Restrict to endpoints using this auth wrapper (e.g. "withAuth"). Pass "none" to list unauthenticated endpoints.' },
34210
+ offset: { type: "number", description: "Pagination offset over the endpoint list. Default 0." },
34211
+ limit: { type: "number", description: "Max endpoints to return per response. Default 10." },
34212
+ project: { type: "string", description: PROJECT_PARAM_DESCRIPTION },
34213
+ worktree: { type: "string", description: WORKTREE_PARAM_DESCRIPTION2 },
34214
+ project_root: { type: "string", description: PROJECT_ROOT_PARAM_DESCRIPTION2 }
34215
+ }
34216
+ }
34217
+ },
34095
34218
  {
34096
34219
  name: "detect_project_stack",
34097
34220
  description: "Detect project languages, frameworks, available parsers, and recommend parser configuration. Scans the project to identify all languages present (TypeScript, Python, Go, etc.) and reports which are supported by registered parsers vs detected-but-unsupported. Also detects frameworks (Next.js, Prisma, React, etc.), provides cross-layer detection stats (fetch calls, @api annotations, URL literals), and returns available graph layers. \n\nUse this when setting up launch-chart for a new project, reviewing parser configuration, or checking what languages are in the project.",
@@ -34764,9 +34887,13 @@ function runReadGraphQueryRaw(rootDir, args) {
34764
34887
  const minimal = args.minimal ?? layerIsDb;
34765
34888
  const includeEdges = args.include_edges;
34766
34889
  const includeFindings = args.include_findings === true;
34890
+ const tagPredicates = Array.isArray(args.tag_predicates) ? args.tag_predicates : void 0;
34891
+ const includeCrossRefs = args.include_cross_refs === true;
34892
+ const crossRefType = args.cross_ref_type;
34893
+ const edgeConfidence = Array.isArray(args.edge_confidence) ? new Set(args.edge_confidence.map((s) => String(s).toLowerCase())) : void 0;
34767
34894
  const offset = args.offset ?? 0;
34768
34895
  const limit = args.limit;
34769
- const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
34896
+ const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue || tagPredicates && tagPredicates.length > 0);
34770
34897
  if (layer) {
34771
34898
  const available = getAvailableLayers(rootDir);
34772
34899
  if (available.length > 0 && !available.includes(layer)) {
@@ -34817,6 +34944,14 @@ function runReadGraphQueryRaw(rootDir, args) {
34817
34944
  result2.contradictions = graph.contradictions ?? [];
34818
34945
  result2.flagged_edges = graph.flagged_edges ?? [];
34819
34946
  }
34947
+ const neighborhoodIds = new Set(nb.nodes.map((n) => n.id));
34948
+ attachCrossRefsAndFlagged(result2, graph, neighborhoodIds, {
34949
+ includeCrossRefs,
34950
+ crossRefType,
34951
+ edgeConfidence,
34952
+ scopeToIds: true,
34953
+ layer
34954
+ });
34820
34955
  return result2;
34821
34956
  }
34822
34957
  if (!hasFilter) {
@@ -34837,6 +34972,11 @@ function runReadGraphQueryRaw(rootDir, args) {
34837
34972
  const nodeTags = n.tags;
34838
34973
  if (module_ && nodeTags?.module !== module_) return false;
34839
34974
  if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
34975
+ if (tagPredicates && tagPredicates.length > 0) {
34976
+ for (const p of tagPredicates) {
34977
+ if (!evaluatePredicate(n, p)) return false;
34978
+ }
34979
+ }
34840
34980
  return true;
34841
34981
  });
34842
34982
  if (matched.length === 0) {
@@ -34884,8 +35024,89 @@ function runReadGraphQueryRaw(rootDir, args) {
34884
35024
  result.contradictions = graph.contradictions ?? [];
34885
35025
  result.flagged_edges = graph.flagged_edges ?? [];
34886
35026
  }
35027
+ attachCrossRefsAndFlagged(result, graph, returnedIds, {
35028
+ includeCrossRefs,
35029
+ crossRefType,
35030
+ edgeConfidence,
35031
+ scopeToIds: true,
35032
+ layer
35033
+ });
34887
35034
  return result;
34888
35035
  }
35036
+ function attachCrossRefsAndFlagged(result, graph, scopeIds, opts) {
35037
+ if (opts.includeCrossRefs) {
35038
+ const allCrossRefs = graph.cross_refs ?? [];
35039
+ const crossRefsByNode = {};
35040
+ for (const id of scopeIds) {
35041
+ crossRefsByNode[id] = { outgoing: [], incoming: [] };
35042
+ }
35043
+ for (const cr of allCrossRefs) {
35044
+ if (opts.crossRefType && cr.type !== opts.crossRefType) continue;
35045
+ if (crossRefsByNode[cr.source]) crossRefsByNode[cr.source].outgoing.push(cr);
35046
+ if (crossRefsByNode[cr.target]) crossRefsByNode[cr.target].incoming.push(cr);
35047
+ }
35048
+ result.cross_refs = crossRefsByNode;
35049
+ if (opts.crossRefType) result.cross_ref_type = opts.crossRefType;
35050
+ if (allCrossRefs.length === 0) {
35051
+ result.parser_warning = parserWarning([opts.layer], "cross_refs", "cross-layer references parser(s) (fetch-resolver / api-annotations / url-literal-scanner / static-ref-scanner)");
35052
+ }
35053
+ }
35054
+ if (opts.edgeConfidence && opts.edgeConfidence.size > 0) {
35055
+ const allFlagged = graph.flagged_edges ?? [];
35056
+ const useScope = opts.scopeToIds && scopeIds.size > 0;
35057
+ result.flagged_edges = allFlagged.filter((fe) => {
35058
+ if (!opts.edgeConfidence.has(String(fe.confidence).toLowerCase())) return false;
35059
+ if (useScope && !scopeIds.has(fe.source) && !scopeIds.has(fe.target)) return false;
35060
+ return true;
35061
+ });
35062
+ if (allFlagged.length === 0 && !result.parser_warning) {
35063
+ result.parser_warning = parserWarning([opts.layer], "flagged_edges", opts.layer === "db" ? "sql-migrations parser" : "cross-layer parsers");
35064
+ }
35065
+ }
35066
+ }
35067
+ function parserWarning(layers, capability, parserName) {
35068
+ return `The ${capability} data is empty in scanned layer(s): ${layers.join(", ")}. This likely means the ${parserName} did not produce output for this project \u2014 run detect_project_stack to verify parser configuration, then generate_graph to refresh. (If the parser IS configured and the source data is genuinely empty, ignore this warning.)`;
35069
+ }
35070
+ function evaluatePredicate(node, p) {
35071
+ if (!p.field || !p.op) return true;
35072
+ const tags = node.tags;
35073
+ const fromTag = tags?.[p.field];
35074
+ const fromField = node[p.field];
35075
+ const present = fromTag !== void 0 || fromField !== void 0;
35076
+ if (p.op === "exists") return present;
35077
+ if (p.op === "not_exists") return !present;
35078
+ if (!present) return false;
35079
+ const raw = fromTag !== void 0 ? coerceTagValue(fromTag, p.value) : fromField;
35080
+ switch (p.op) {
35081
+ case "eq":
35082
+ return raw === p.value;
35083
+ case "ne":
35084
+ return raw !== p.value;
35085
+ case "gt":
35086
+ return typeof raw === "number" && typeof p.value === "number" && raw > p.value;
35087
+ case "lt":
35088
+ return typeof raw === "number" && typeof p.value === "number" && raw < p.value;
35089
+ case "gte":
35090
+ return typeof raw === "number" && typeof p.value === "number" && raw >= p.value;
35091
+ case "lte":
35092
+ return typeof raw === "number" && typeof p.value === "number" && raw <= p.value;
35093
+ case "in":
35094
+ return Array.isArray(p.value) && p.value.includes(raw);
35095
+ default:
35096
+ return false;
35097
+ }
35098
+ }
35099
+ function coerceTagValue(raw, target) {
35100
+ if (typeof target === "boolean") {
35101
+ if (raw === "true") return true;
35102
+ if (raw === "false") return false;
35103
+ }
35104
+ if (typeof target === "number") {
35105
+ const n = Number(raw);
35106
+ if (!Number.isNaN(n)) return n;
35107
+ }
35108
+ return raw;
35109
+ }
34889
35110
  function runReadGraphQuery(rootDir, args) {
34890
35111
  const raw = runReadGraphQueryRaw(rootDir, args);
34891
35112
  return compactResult(raw);
@@ -34996,8 +35217,23 @@ function handleInspectNode(args) {
34996
35217
  } else {
34997
35218
  matched = graph.nodes;
34998
35219
  }
34999
- const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects", "ui_labels", "notes"];
35220
+ const allDeepFields = ["elements", "stateVars", "conditions", "variables", "responses", "params", "effects", "ui_labels", "notes", "crossRefs"];
35000
35221
  const requestedFields = fields ?? allDeepFields;
35222
+ const crossRefTypeArg = args.cross_ref_type;
35223
+ const wantCrossRefs = requestedFields.includes("crossRefs");
35224
+ let crossRefsBySource = null;
35225
+ let crossRefsByTarget = null;
35226
+ if (wantCrossRefs) {
35227
+ crossRefsBySource = /* @__PURE__ */ new Map();
35228
+ crossRefsByTarget = /* @__PURE__ */ new Map();
35229
+ for (const cr of graph.cross_refs ?? []) {
35230
+ if (crossRefTypeArg && cr.type !== crossRefTypeArg) continue;
35231
+ if (!crossRefsBySource.has(cr.source)) crossRefsBySource.set(cr.source, []);
35232
+ crossRefsBySource.get(cr.source).push(cr);
35233
+ if (!crossRefsByTarget.has(cr.target)) crossRefsByTarget.set(cr.target, []);
35234
+ crossRefsByTarget.get(cr.target).push(cr);
35235
+ }
35236
+ }
35001
35237
  let filterRegex = null;
35002
35238
  if (filter) {
35003
35239
  try {
@@ -35020,7 +35256,14 @@ function handleInspectNode(args) {
35020
35256
  const deep = { id: node.id, name: node.name, type: node.type };
35021
35257
  let hasData = false;
35022
35258
  for (const field of requestedFields) {
35023
- if (allDeepFields.includes(field) && node[field] != null) {
35259
+ if (field === "crossRefs") {
35260
+ const outgoing = crossRefsBySource?.get(node.id) ?? [];
35261
+ const incoming = crossRefsByTarget?.get(node.id) ?? [];
35262
+ if (outgoing.length > 0 || incoming.length > 0) {
35263
+ deep.crossRefs = { outgoing, incoming };
35264
+ hasData = true;
35265
+ }
35266
+ } else if (allDeepFields.includes(field) && node[field] != null) {
35024
35267
  deep[field] = node[field];
35025
35268
  hasData = true;
35026
35269
  }
@@ -35044,10 +35287,14 @@ function handleInspectNode(args) {
35044
35287
  const hint = filter ? `No nodes with deep fields matching /${filter}/${caseInsensitive ? "i" : ""} in ${layer} layer.` : `No nodes matching "${search}" in ${layer} layer.`;
35045
35288
  return err(hint);
35046
35289
  }
35290
+ const wantedCrossRefs = wantCrossRefs;
35291
+ const graphHasNoCrossRefs = (graph.cross_refs ?? []).length === 0;
35292
+ const extraWarning = wantedCrossRefs && graphHasNoCrossRefs ? { parser_warning: parserWarning([layer], "cross_refs", "cross-layer references parser(s) (fetch-resolver / api-annotations / url-literal-scanner / static-ref-scanner)") } : {};
35047
35293
  return okJson({
35048
35294
  layer,
35049
35295
  matched: results.length,
35050
35296
  ...results.length >= maxResults ? { truncated: true, hint: `Showing first ${maxResults} matches. Narrow with search param.` } : {},
35297
+ ...extraWarning,
35051
35298
  nodes: results
35052
35299
  });
35053
35300
  }
@@ -35253,13 +35500,19 @@ function handleEffectsIndex(args) {
35253
35500
  const __resolved = resolveOrErr(args);
35254
35501
  if ("content" in __resolved) return __resolved;
35255
35502
  const { rootDir } = __resolved;
35503
+ const kind = args.kind ?? "collisions";
35504
+ const key = args.key;
35505
+ const MAX_KEYS = 200;
35506
+ if (kind === "fetch_calls") {
35507
+ return okJson(buildFetchCallsIndex(rootDir, key, MAX_KEYS));
35508
+ }
35509
+ if (kind === "api_effects") {
35510
+ return okJson(buildApiEffectsIndex(rootDir, key, MAX_KEYS));
35511
+ }
35256
35512
  const idx = readEffectsIndex(rootDir);
35257
35513
  if (!idx) {
35258
35514
  return err("No effects-index.json found. Run generate_graph first (or wait for the file-watcher to fire).");
35259
35515
  }
35260
- const kind = args.kind ?? "collisions";
35261
- const key = args.key;
35262
- const MAX_KEYS = 200;
35263
35516
  if (kind === "singleton_risks") {
35264
35517
  return okJson({ kind, count: idx.singleton_risks.length, nodes: idx.singleton_risks });
35265
35518
  }
@@ -35273,7 +35526,7 @@ function handleEffectsIndex(args) {
35273
35526
  }
35274
35527
  const map = idx[kind];
35275
35528
  if (!map || typeof map !== "object") {
35276
- return err(`Unknown kind "${kind}". Valid: dom_ids, window_events, storage_keys, fetch_urls, timers, singleton_risks, collisions.`);
35529
+ return err(`Unknown kind "${kind}". Valid: dom_ids, window_events, storage_keys, fetch_urls, timers, singleton_risks, collisions, fetch_calls, api_effects.`);
35277
35530
  }
35278
35531
  if (key) {
35279
35532
  const nodes = map[key] ?? [];
@@ -35291,6 +35544,97 @@ function handleEffectsIndex(args) {
35291
35544
  results
35292
35545
  });
35293
35546
  }
35547
+ function buildFetchCallsIndex(rootDir, key, maxKeys) {
35548
+ const ui = readGraph(rootDir, "ui");
35549
+ if (!ui) return { kind: "fetch_calls", error: "No ui graph found. Run generate_graph first." };
35550
+ const inverted = /* @__PURE__ */ new Map();
35551
+ let callsApiCount = 0;
35552
+ for (const cr of ui.cross_refs ?? []) {
35553
+ if (cr.type !== "calls_api") continue;
35554
+ callsApiCount++;
35555
+ if (!inverted.has(cr.target)) inverted.set(cr.target, /* @__PURE__ */ new Set());
35556
+ inverted.get(cr.target).add(cr.source);
35557
+ }
35558
+ for (const n of ui.nodes) {
35559
+ const eff = n.effects;
35560
+ if (!eff?.fetches) continue;
35561
+ for (const f of eff.fetches) {
35562
+ if (!inverted.has(f)) inverted.set(f, /* @__PURE__ */ new Set());
35563
+ inverted.get(f).add(n.id);
35564
+ }
35565
+ }
35566
+ const allCrossRefs = ui.cross_refs ?? [];
35567
+ const fetchResolverAbsent = callsApiCount === 0 && allCrossRefs.length === 0;
35568
+ const partialResolverGap = callsApiCount === 0 && inverted.size > 0;
35569
+ if (key) {
35570
+ return {
35571
+ kind: "fetch_calls",
35572
+ key,
35573
+ callers: [...inverted.get(key) ?? []],
35574
+ ...fetchResolverAbsent ? { parser_warning: parserWarning(["ui"], "cross_refs", "fetch-resolver cross-layer parser") } : {},
35575
+ ...partialResolverGap ? { parser_warning: "No calls_api cross_refs found \u2014 fetch-resolver may not be enabled. Only raw fetch() strings from ui.node.effects.fetches are indexed; resolved-to-endpoint links are missing." } : {}
35576
+ };
35577
+ }
35578
+ const entries = [...inverted.entries()].sort((a, b) => b[1].size - a[1].size);
35579
+ const truncated = entries.length > maxKeys;
35580
+ const results = {};
35581
+ for (const [k, v] of entries.slice(0, maxKeys)) results[k] = [...v];
35582
+ return {
35583
+ kind: "fetch_calls",
35584
+ total_keys: entries.length,
35585
+ ...truncated ? { truncated: true, showing: maxKeys, hint: "Pass `key` (endpoint path or URL template) to look up callers." } : {},
35586
+ ...fetchResolverAbsent ? { parser_warning: parserWarning(["ui"], "cross_refs", "fetch-resolver cross-layer parser") } : {},
35587
+ ...partialResolverGap ? { parser_warning: "No calls_api cross_refs found \u2014 fetch-resolver may not be enabled. Only raw fetch() strings from ui.node.effects.fetches are indexed; resolved-to-endpoint links are missing." } : {},
35588
+ results
35589
+ };
35590
+ }
35591
+ function buildApiEffectsIndex(rootDir, key, maxKeys) {
35592
+ const api = readGraph(rootDir, "api");
35593
+ if (!api) return { kind: "api_effects", error: "No api graph found. Run generate_graph first." };
35594
+ const categories = ["calls", "fetches", "persists", "subscribes", "timers", "dom_writes", "globals"];
35595
+ const inverted = {};
35596
+ for (const c of categories) inverted[c] = /* @__PURE__ */ new Map();
35597
+ for (const n of api.nodes) {
35598
+ const eff = n.effects;
35599
+ if (!eff) continue;
35600
+ for (const c of categories) {
35601
+ const items = eff[c];
35602
+ if (!Array.isArray(items)) continue;
35603
+ for (const item of items) {
35604
+ if (!inverted[c].has(item)) inverted[c].set(item, /* @__PURE__ */ new Set());
35605
+ inverted[c].get(item).add(n.id);
35606
+ }
35607
+ }
35608
+ }
35609
+ if (key) {
35610
+ const hits = {};
35611
+ for (const c of categories) {
35612
+ const endpoints = inverted[c].get(key);
35613
+ if (endpoints && endpoints.size > 0) hits[c] = [...endpoints];
35614
+ }
35615
+ return { kind: "api_effects", key, hits };
35616
+ }
35617
+ const TOP_CATEGORIES = Math.min(maxKeys, 50);
35618
+ const TOP_CALLERS = 10;
35619
+ const summary = {};
35620
+ for (const c of categories) {
35621
+ const entries = [...inverted[c].entries()].sort((a, b) => b[1].size - a[1].size);
35622
+ const top = {};
35623
+ for (const [k, v] of entries.slice(0, TOP_CATEGORIES)) {
35624
+ const endpoints = [...v];
35625
+ top[k] = {
35626
+ count: endpoints.length,
35627
+ sample_endpoints: endpoints.slice(0, TOP_CALLERS)
35628
+ };
35629
+ }
35630
+ summary[c] = { total_items: entries.length, top };
35631
+ }
35632
+ return {
35633
+ kind: "api_effects",
35634
+ hint: `Summary mode: top ${TOP_CATEGORIES} items per category, top ${TOP_CALLERS} endpoints per item. Pass \`key\` (a specific call/url/event) to get the FULL caller list across all categories.`,
35635
+ summary
35636
+ };
35637
+ }
35294
35638
  function handleChartServerStatus() {
35295
35639
  const rootDir = process.cwd();
35296
35640
  const lock = getLiveLock(rootDir);
@@ -35404,6 +35748,296 @@ function handleRemoveTag(args) {
35404
35748
  removeTag(rootDir, nodeId, key);
35405
35749
  return okJson({ ok: true, node_id: nodeId, removed_key: key });
35406
35750
  }
35751
+ function handleMigrationAudit(args) {
35752
+ const __resolved = resolveOrErr(args);
35753
+ if ("content" in __resolved) return __resolved;
35754
+ const { rootDir } = __resolved;
35755
+ const requireOrphan = args.require_orphan_check !== false;
35756
+ const requireBackup = args.require_sidecar_backup !== false;
35757
+ const requirePreFlight = args.require_pre_flight_notice !== false;
35758
+ const includeSafe = args.include_safe === true;
35759
+ const offset = args.offset ?? 0;
35760
+ const limit = args.limit ?? 10;
35761
+ const db = readGraph(rootDir, "db");
35762
+ if (!db) return err("No db graph found. Run generate_graph first.");
35763
+ const migrations = db.nodes.filter((n) => n.type === "migration");
35764
+ const audit = [];
35765
+ for (const m of migrations) {
35766
+ const isDestructive = m.is_destructive === true;
35767
+ if (!isDestructive && !includeSafe) continue;
35768
+ const violations = [];
35769
+ if (isDestructive) {
35770
+ if (requireOrphan && m.has_orphan_check !== true) violations.push("missing_orphan_check");
35771
+ if (requireBackup && m.has_sidecar_backup !== true) violations.push("missing_sidecar_backup");
35772
+ if (requirePreFlight && m.has_pre_flight_notice !== true) violations.push("missing_pre_flight_notice");
35773
+ }
35774
+ if (!isDestructive && includeSafe && violations.length === 0) {
35775
+ audit.push({ id: m.id, name: m.name, timestamp: m.timestamp, is_destructive: false, status: "safe" });
35776
+ continue;
35777
+ }
35778
+ if (violations.length === 0 && !includeSafe) continue;
35779
+ audit.push({
35780
+ id: m.id,
35781
+ name: m.name,
35782
+ timestamp: m.timestamp,
35783
+ filepath: m.filepath,
35784
+ is_destructive: isDestructive,
35785
+ contains_drop_column: m.contains_drop_column === true,
35786
+ contains_drop_table: m.contains_drop_table === true,
35787
+ contains_backfill: m.contains_backfill === true,
35788
+ has_orphan_check: m.has_orphan_check === true,
35789
+ has_sidecar_backup: m.has_sidecar_backup === true,
35790
+ has_pre_flight_notice: m.has_pre_flight_notice === true,
35791
+ statement_count: m.statement_count,
35792
+ violations,
35793
+ status: violations.length === 0 ? "safe" : "unsafe"
35794
+ });
35795
+ }
35796
+ audit.sort((a, b) => String(b.timestamp ?? "").localeCompare(String(a.timestamp ?? "")));
35797
+ const total = audit.length;
35798
+ const page = audit.slice(offset, offset + limit);
35799
+ const hasMore = offset + page.length < total;
35800
+ return okJson({
35801
+ total,
35802
+ returned: page.length,
35803
+ offset,
35804
+ has_more: hasMore,
35805
+ ...hasMore ? { next_offset: offset + page.length } : {},
35806
+ items: page,
35807
+ discipline: "Layer 1 (expand-and-contract) + Layer 2 (pg_dump wrapper) + Layer 3 (in-migration SQL guards) per CLAUDE.md. This tool checks Layer 3 only."
35808
+ });
35809
+ }
35810
+ function handleDriftReport(args) {
35811
+ const __resolved = resolveOrErr(args);
35812
+ if ("content" in __resolved) return __resolved;
35813
+ const { rootDir } = __resolved;
35814
+ const layerArg = args.layer;
35815
+ const kind = args.kind ?? "all";
35816
+ const confidenceArg = Array.isArray(args.confidence) ? new Set(args.confidence.map((s) => String(s).toLowerCase())) : void 0;
35817
+ const offset = args.offset ?? 0;
35818
+ const limit = args.limit ?? 10;
35819
+ const layers = layerArg ? [layerArg] : getAvailableLayers(rootDir);
35820
+ const items = [];
35821
+ const layersWithAnyFinding = [];
35822
+ for (const layer of layers) {
35823
+ const g = readGraph(rootDir, layer);
35824
+ if (!g) continue;
35825
+ const cs = g.contradictions ?? [];
35826
+ const fes = g.flagged_edges ?? [];
35827
+ if (cs.length > 0 || fes.length > 0) layersWithAnyFinding.push(layer);
35828
+ if (kind === "all" || kind === "contradictions") {
35829
+ for (const c of cs) {
35830
+ items.push({ layer, kind: "contradiction", entity: c.entity, source_a: c.source_a, source_b: c.source_b, detail: c.detail });
35831
+ }
35832
+ }
35833
+ if (kind === "all" || kind === "flagged_edges") {
35834
+ for (const fe of fes) {
35835
+ if (confidenceArg && !confidenceArg.has(String(fe.confidence).toLowerCase())) continue;
35836
+ items.push({
35837
+ layer,
35838
+ kind: "flagged_edge",
35839
+ source: fe.source,
35840
+ target: fe.target,
35841
+ edge_type: fe.type,
35842
+ confidence: fe.confidence,
35843
+ label: fe.label
35844
+ });
35845
+ }
35846
+ }
35847
+ }
35848
+ const total = items.length;
35849
+ const page = items.slice(offset, offset + limit);
35850
+ const hasMore = offset + page.length < total;
35851
+ const parserAbsent = layers.length > 0 && layersWithAnyFinding.length === 0;
35852
+ return okJson({
35853
+ total,
35854
+ returned: page.length,
35855
+ offset,
35856
+ has_more: hasMore,
35857
+ ...hasMore ? { next_offset: offset + page.length } : {},
35858
+ layers_scanned: layers,
35859
+ ...parserAbsent ? { parser_warning: parserWarning(layers, "contradictions + flagged_edges", layers.includes("db") ? "sql-migrations parser and/or cross-layer parsers" : "cross-layer parsers") } : {},
35860
+ items: page
35861
+ });
35862
+ }
35863
+ function handleWhoUses(args) {
35864
+ const __resolved = resolveOrErr(args);
35865
+ if ("content" in __resolved) return __resolved;
35866
+ const { rootDir } = __resolved;
35867
+ const target = args.target;
35868
+ if (!target) return err("target is required");
35869
+ const filterType = args.cross_ref_type;
35870
+ const offset = args.offset ?? 0;
35871
+ const limit = args.limit ?? 10;
35872
+ const hits = [];
35873
+ const layersScanned = [];
35874
+ const layersWithCrossRefs = [];
35875
+ for (const layer of getAvailableLayers(rootDir)) {
35876
+ const g = readGraph(rootDir, layer);
35877
+ if (!g) continue;
35878
+ layersScanned.push(layer);
35879
+ const crs = g.cross_refs ?? [];
35880
+ if (crs.length > 0) layersWithCrossRefs.push(layer);
35881
+ for (const cr of crs) {
35882
+ if (cr.target !== target) continue;
35883
+ if (filterType && cr.type !== filterType) continue;
35884
+ const entry = {
35885
+ source_layer: layer,
35886
+ source: cr.source,
35887
+ cross_ref_type: cr.type,
35888
+ target_layer: cr.layer
35889
+ };
35890
+ const via = cr.via;
35891
+ if (Array.isArray(via)) entry.via = via;
35892
+ hits.push(entry);
35893
+ }
35894
+ }
35895
+ hits.sort((a, b) => String(a.source).localeCompare(String(b.source)));
35896
+ const total = hits.length;
35897
+ const page = hits.slice(offset, offset + limit);
35898
+ const hasMore = offset + page.length < total;
35899
+ const parserAbsent = layersScanned.length > 0 && layersWithCrossRefs.length === 0;
35900
+ return okJson({
35901
+ target,
35902
+ ...filterType ? { cross_ref_type: filterType } : {},
35903
+ total,
35904
+ returned: page.length,
35905
+ offset,
35906
+ has_more: hasMore,
35907
+ ...hasMore ? { next_offset: offset + page.length } : {},
35908
+ ...parserAbsent ? { parser_warning: parserWarning(layersScanned, "cross_refs", "cross-layer references parser(s) (fetch-resolver / api-annotations / url-literal-scanner / static-ref-scanner)") } : {},
35909
+ items: page
35910
+ });
35911
+ }
35912
+ function handleTracePath(args) {
35913
+ const __resolved = resolveOrErr(args);
35914
+ if ("content" in __resolved) return __resolved;
35915
+ const { rootDir } = __resolved;
35916
+ const from = args.from;
35917
+ const to = args.to;
35918
+ if (!from || !to) return err("from and to are required");
35919
+ const maxHops = args.max_hops ?? 5;
35920
+ const maxPaths = args.max_paths ?? 10;
35921
+ const offset = args.offset ?? 0;
35922
+ const limit = args.limit ?? 10;
35923
+ const outgoing = /* @__PURE__ */ new Map();
35924
+ const layersScanned = [];
35925
+ const layersWithCrossRefs = [];
35926
+ for (const layer of getAvailableLayers(rootDir)) {
35927
+ const g = readGraph(rootDir, layer);
35928
+ if (!g) continue;
35929
+ layersScanned.push(layer);
35930
+ if ((g.cross_refs ?? []).length > 0) layersWithCrossRefs.push(layer);
35931
+ for (const cr of g.cross_refs ?? []) {
35932
+ const via = cr.via;
35933
+ if (Array.isArray(via) && via.length > 0) {
35934
+ let prev = cr.source;
35935
+ for (const step of via) {
35936
+ pushEdge(outgoing, { from: prev, to: step, type: cr.type, layer: cr.layer });
35937
+ prev = step;
35938
+ }
35939
+ pushEdge(outgoing, { from: prev, to: cr.target, type: cr.type, layer: cr.layer });
35940
+ } else {
35941
+ pushEdge(outgoing, { from: cr.source, to: cr.target, type: cr.type, layer: cr.layer });
35942
+ }
35943
+ }
35944
+ for (const e of g.edges ?? []) {
35945
+ pushEdge(outgoing, { from: e.source, to: e.target, type: e.type, layer });
35946
+ }
35947
+ }
35948
+ const paths = [];
35949
+ const queue = [{ nodes: [from], edges: [] }];
35950
+ while (queue.length > 0 && paths.length < maxPaths) {
35951
+ const p = queue.shift();
35952
+ const tail = p.nodes[p.nodes.length - 1];
35953
+ if (tail === to && p.nodes.length > 1) {
35954
+ paths.push(p);
35955
+ continue;
35956
+ }
35957
+ if (p.nodes.length > maxHops) continue;
35958
+ const next = outgoing.get(tail) ?? [];
35959
+ for (const e of next) {
35960
+ if (p.nodes.includes(e.to)) continue;
35961
+ queue.push({ nodes: [...p.nodes, e.to], edges: [...p.edges, e] });
35962
+ }
35963
+ }
35964
+ paths.sort((a, b) => a.nodes.length - b.nodes.length);
35965
+ const total = paths.length;
35966
+ const page = paths.slice(offset, offset + limit);
35967
+ const hasMore = offset + page.length < total;
35968
+ const parserAbsent = layersScanned.length > 0 && layersWithCrossRefs.length === 0;
35969
+ return okJson({
35970
+ from,
35971
+ to,
35972
+ max_hops: maxHops,
35973
+ max_paths: maxPaths,
35974
+ total,
35975
+ returned: page.length,
35976
+ offset,
35977
+ has_more: hasMore,
35978
+ ...hasMore ? { next_offset: offset + page.length } : {},
35979
+ ...total === maxPaths ? { search_capped: true, hint: "Hit max_paths cap \u2014 more paths may exist. Raise max_paths or narrow from/to." } : {},
35980
+ ...parserAbsent ? { parser_warning: parserWarning(layersScanned, "cross_refs", "cross-layer references parser(s)") + " Cross-layer paths are unreachable without cross_refs; only same-layer paths (imports/renders/belongs_to) are visible." } : {},
35981
+ paths: page.map((p) => ({ hops: p.nodes.length - 1, nodes: p.nodes, edges: p.edges }))
35982
+ });
35983
+ }
35984
+ function pushEdge(map, e) {
35985
+ if (!map.has(e.from)) map.set(e.from, []);
35986
+ map.get(e.from).push(e);
35987
+ }
35988
+ function handleAuthCoverageReport(args) {
35989
+ const __resolved = resolveOrErr(args);
35990
+ if ("content" in __resolved) return __resolved;
35991
+ const { rootDir } = __resolved;
35992
+ const moduleFilter = args.module;
35993
+ const strategyFilter = args.strategy;
35994
+ const offset = args.offset ?? 0;
35995
+ const limit = args.limit ?? 10;
35996
+ const api = readGraph(rootDir, "api");
35997
+ if (!api) return err("No api graph found. Run generate_graph first.");
35998
+ const byStrategy = {};
35999
+ const byModule = {};
36000
+ const unauthenticated = [];
36001
+ const matchedEndpoints = [];
36002
+ for (const n of api.nodes) {
36003
+ const tags = n.tags;
36004
+ const moduleTag = tags?.module ?? "root";
36005
+ const auth = n.auth ?? [];
36006
+ const strategyKey = auth.length === 0 ? "none" : auth.slice().sort().join("+");
36007
+ byStrategy[strategyKey] = (byStrategy[strategyKey] ?? 0) + 1;
36008
+ if (!byModule[moduleTag]) byModule[moduleTag] = { total: 0, by_strategy: {} };
36009
+ byModule[moduleTag].total += 1;
36010
+ byModule[moduleTag].by_strategy[strategyKey] = (byModule[moduleTag].by_strategy[strategyKey] ?? 0) + 1;
36011
+ if (auth.length === 0) unauthenticated.push(n.id);
36012
+ if (moduleFilter && moduleTag !== moduleFilter) continue;
36013
+ if (strategyFilter && strategyKey !== strategyFilter) continue;
36014
+ matchedEndpoints.push({
36015
+ id: n.id,
36016
+ path: n.path,
36017
+ methods: n.methods,
36018
+ module: moduleTag,
36019
+ auth,
36020
+ mutates: n.mutates === true
36021
+ });
36022
+ }
36023
+ matchedEndpoints.sort((a, b) => String(a.path ?? a.id).localeCompare(String(b.path ?? b.id)));
36024
+ const total = matchedEndpoints.length;
36025
+ const page = matchedEndpoints.slice(offset, offset + limit);
36026
+ const hasMore = offset + page.length < total;
36027
+ return okJson({
36028
+ total_endpoints: api.nodes.length,
36029
+ by_strategy: byStrategy,
36030
+ unauthenticated_count: unauthenticated.length,
36031
+ by_module: byModule,
36032
+ filter: { module: moduleFilter, strategy: strategyFilter },
36033
+ total,
36034
+ returned: page.length,
36035
+ offset,
36036
+ has_more: hasMore,
36037
+ ...hasMore ? { next_offset: offset + page.length } : {},
36038
+ items: page
36039
+ });
36040
+ }
35407
36041
  function handleAuditLayer(args) {
35408
36042
  const __resolved = resolveOrErr(args);
35409
36043
  if ("content" in __resolved) return __resolved;
@@ -35617,6 +36251,26 @@ async function handleMessage(msg) {
35617
36251
  respond(id ?? null, withFreshnessMeta(handleListNotes(args), args));
35618
36252
  return;
35619
36253
  }
36254
+ if (toolName === "migration_audit") {
36255
+ respond(id ?? null, withFreshnessMeta(handleMigrationAudit(args), args));
36256
+ return;
36257
+ }
36258
+ if (toolName === "drift_report") {
36259
+ respond(id ?? null, withFreshnessMeta(handleDriftReport(args), args));
36260
+ return;
36261
+ }
36262
+ if (toolName === "who_uses") {
36263
+ respond(id ?? null, withFreshnessMeta(handleWhoUses(args), args));
36264
+ return;
36265
+ }
36266
+ if (toolName === "trace_path") {
36267
+ respond(id ?? null, withFreshnessMeta(handleTracePath(args), args));
36268
+ return;
36269
+ }
36270
+ if (toolName === "auth_coverage_report") {
36271
+ respond(id ?? null, withFreshnessMeta(handleAuthCoverageReport(args), args));
36272
+ return;
36273
+ }
35620
36274
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
35621
36275
  return;
35622
36276
  }